diff --git a/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/Buffer.java b/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/Buffer.java index 83159a5a05..332cfb9e43 100644 --- a/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/Buffer.java +++ b/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/Buffer.java @@ -1657,6 +1657,21 @@ default boolean tryEnsureWritable(int minWritableBytes, boolean force) { */ Buffer writeUtf8(CharSequence seq, int ensureWritable); + /** + * Encode a {@link CharSequence} encoded in {@link Charset} and write it to this buffer starting at + * {@code writerIndex} and increases the {@code writerIndex} by the number of the transferred bytes. + * + * @param seq the source of the data. + * @param charset the charset used for encoding. + * @return self. + * @throws ReadOnlyBufferException if this buffer is read-only + */ + default Buffer writeCharSequence(CharSequence seq, Charset charset) { + byte[] bytes = seq.toString().getBytes(charset); + writeBytes(bytes); + return this; + } + /** * Locates the first occurrence of the specified {@code value} in this * buffer. The search takes place from the specified {@code fromIndex} diff --git a/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/EmptyBuffer.java b/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/EmptyBuffer.java index 25d036d426..534e7be987 100644 --- a/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/EmptyBuffer.java +++ b/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/EmptyBuffer.java @@ -644,6 +644,11 @@ public Buffer writeUtf8(CharSequence seq, int ensureWritable) { throw new IndexOutOfBoundsException(); } + @Override + public Buffer writeCharSequence(CharSequence seq, Charset charset) { + throw new IndexOutOfBoundsException(); + } + @Override public int indexOf(int fromIndex, int toIndex, byte value) { checkIndex(fromIndex); diff --git a/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/ReadOnlyByteBuffer.java b/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/ReadOnlyByteBuffer.java index 73db2755f1..89c0451b6e 100644 --- a/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/ReadOnlyByteBuffer.java +++ b/servicetalk-buffer-api/src/main/java/io/servicetalk/buffer/api/ReadOnlyByteBuffer.java @@ -375,6 +375,11 @@ public Buffer writeUtf8(CharSequence seq, int ensureWritable) { throw new ReadOnlyBufferException(); } + @Override + public Buffer writeCharSequence(CharSequence seq, Charset charset) { + throw new ReadOnlyBufferException(); + } + @Override public Buffer readSlice(int length) { checkReadableBytes0(length); diff --git a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyBuffer.java b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyBuffer.java index a6f4cf99df..dddb8b3d2f 100644 --- a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyBuffer.java +++ b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyBuffer.java @@ -710,6 +710,12 @@ public Buffer writeUtf8(CharSequence seq, int ensureWritable) { return this; } + @Override + public Buffer writeCharSequence(CharSequence seq, Charset charset) { + buffer.writeCharSequence(seq, charset); + return this; + } + @Override public int indexOf(int fromIndex, int toIndex, byte value) { return buffer.indexOf(fromIndex, toIndex, value); diff --git a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyCompositeBuffer.java b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyCompositeBuffer.java index 4a8f510ff2..42d51fb45a 100644 --- a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyCompositeBuffer.java +++ b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/NettyCompositeBuffer.java @@ -21,6 +21,7 @@ import io.netty.buffer.CompositeByteBuf; import java.nio.ByteBuffer; +import java.nio.charset.Charset; final class NettyCompositeBuffer extends NettyBuffer implements CompositeBuffer { @@ -363,4 +364,10 @@ public CompositeBuffer writeUtf8(CharSequence seq, int ensureWritable) { super.writeUtf8(seq, ensureWritable); return this; } + + @Override + public CompositeBuffer writeCharSequence(CharSequence seq, Charset charset) { + super.writeCharSequence(seq, charset); + return this; + } } diff --git a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/ReadOnlyBuffer.java b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/ReadOnlyBuffer.java index 4f76ef477d..17a66ab075 100644 --- a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/ReadOnlyBuffer.java +++ b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/ReadOnlyBuffer.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ReadOnlyBufferException; +import java.nio.charset.Charset; final class ReadOnlyBuffer extends WrappedBuffer { @@ -302,6 +303,11 @@ public Buffer writeUtf8(CharSequence seq, int ensureWritable) { throw new ReadOnlyBufferException(); } + @Override + public Buffer writeCharSequence(CharSequence seq, Charset charset) { + throw new ReadOnlyBufferException(); + } + @Override public Buffer readSlice(int length) { return buffer.readSlice(length).asReadOnly(); diff --git a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/WrappedBuffer.java b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/WrappedBuffer.java index 8ab8d2afa9..6628bc7681 100644 --- a/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/WrappedBuffer.java +++ b/servicetalk-buffer-netty/src/main/java/io/servicetalk/buffer/netty/WrappedBuffer.java @@ -658,6 +658,12 @@ public Buffer writeUtf8(CharSequence seq, int ensureWritable) { return this; } + @Override + public Buffer writeCharSequence(CharSequence seq, Charset charset) { + buffer.writeCharSequence(seq, charset); + return this; + } + @Override public int indexOf(int fromIndex, int toIndex, byte value) { return buffer.indexOf(fromIndex, toIndex, value); diff --git a/servicetalk-concurrent-reactivestreams/build.gradle b/servicetalk-concurrent-reactivestreams/build.gradle index b0aaaa4450..69c7588786 100644 --- a/servicetalk-concurrent-reactivestreams/build.gradle +++ b/servicetalk-concurrent-reactivestreams/build.gradle @@ -21,6 +21,8 @@ dependencies { api "org.reactivestreams:reactive-streams:$reactiveStreamsVersion" implementation project(":servicetalk-annotations") + implementation project(":servicetalk-serializer-utils") + implementation project(":servicetalk-buffer-netty") implementation "com.google.code.findbugs:jsr305:$jsr305Version" implementation "org.slf4j:slf4j-api:$slf4jVersion" diff --git a/servicetalk-concurrent-reactivestreams/src/test/java/io/servicetalk/concurrent/reactivestreams/tck/FramedDeserializerOperatorTckTest.java b/servicetalk-concurrent-reactivestreams/src/test/java/io/servicetalk/concurrent/reactivestreams/tck/FramedDeserializerOperatorTckTest.java new file mode 100644 index 0000000000..51d4f9b8db --- /dev/null +++ b/servicetalk-concurrent-reactivestreams/src/test/java/io/servicetalk/concurrent/reactivestreams/tck/FramedDeserializerOperatorTckTest.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2021 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.concurrent.reactivestreams.tck; + +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.serializer.utils.FramedDeserializerOperator; + +import org.testng.annotations.Test; + +import static io.servicetalk.buffer.netty.BufferAllocators.DEFAULT_ALLOCATOR; +import static java.util.function.Function.identity; + +@Test +public class FramedDeserializerOperatorTckTest extends AbstractPublisherTckTest { + @Override + public Publisher createServiceTalkPublisher(final long elements) { + return Publisher.range(0, TckUtils.requestNToInt(elements)) + .map(i -> DEFAULT_ALLOCATOR.newBuffer().writeInt(i)) + .liftSync(new FramedDeserializerOperator<>( + (serializedData, allocator) -> serializedData.readInt(), + () -> (buffer, bufferAllocator) -> + buffer.readableBytes() < Integer.BYTES ? null : buffer.readBytes(Integer.BYTES), + DEFAULT_ALLOCATOR)) + .flatMapConcatIterable(identity()); + } + + @Override + public long maxElementsFromPublisher() { + return TckUtils.maxElementsFromPublisher(); + } +} diff --git a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializationProviderContextResolver.java b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializationProviderContextResolver.java index c0c77b7a25..e310cf6ca9 100644 --- a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializationProviderContextResolver.java +++ b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializationProviderContextResolver.java @@ -20,6 +20,10 @@ import javax.annotation.Nullable; import javax.ws.rs.ext.ContextResolver; +/** + * @deprecated Use {@link JacksonSerializerFactoryContextResolver}. + */ +@Deprecated final class JacksonSerializationProviderContextResolver implements ContextResolver { private final JacksonSerializationProvider jacksonSerializationProvider; diff --git a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerFactoryContextResolver.java b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerFactoryContextResolver.java new file mode 100644 index 0000000000..cf11d35e50 --- /dev/null +++ b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/JacksonSerializerFactoryContextResolver.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2021 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.JacksonSerializerFactory; + +import javax.annotation.Nullable; +import javax.ws.rs.ext.ContextResolver; + +import static java.util.Objects.requireNonNull; + +final class JacksonSerializerFactoryContextResolver implements ContextResolver { + private final JacksonSerializerFactory factory; + + JacksonSerializerFactoryContextResolver(final JacksonSerializerFactory factory) { + this.factory = requireNonNull(factory); + } + + @Nullable + @Override + public JacksonSerializerFactory getContext(final Class aClass) { + if (!JacksonSerializerFactory.class.isAssignableFrom(aClass)) { + return null; + } + + return factory; + } +} 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 ca5bc21f07..5dc5dd920e 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,12 +19,13 @@ 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.serialization.api.SerializationException; -import io.servicetalk.serialization.api.Serializer; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.StreamingSerializer; import io.servicetalk.transport.api.ConnectionContext; import io.servicetalk.transport.api.ExecutionContext; @@ -68,9 +69,6 @@ @Consumes(WILDCARD) @Produces(WILDCARD) final class JacksonSerializerMessageBodyReaderWriter implements MessageBodyReader, MessageBodyWriter { - private static final JacksonSerializationProvider DEFAULT_JACKSON_SERIALIZATION_PROVIDER = - new JacksonSerializationProvider(); - // We can not use `@Context ConnectionContext` directly because we would not see the latest version // in case it has been rebound as part of offloading. @Context @@ -95,86 +93,81 @@ 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 Serializer serializer = getSerializer(mediaType); + 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, serializer, getSourceClass(genericType), contentLength, a), - (is, a) -> new SingleSource<>(deserialize(toBufferPublisher(is, a), serializer, - getSourceClass(genericType), contentLength, a))); + (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) -> serializer.deserialize(p, getSourceClass(genericType)), - (is, a) -> new PublisherSource<>(serializer.deserialize(toBufferPublisher(is, a), - getSourceClass(genericType)))); + (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, serializer, type, contentLength, a), - (is, a) -> deserializeObject(toBufferPublisher(is, a), serializer, type, contentLength, a)); + (p, a) -> deserializeObject(p, serializerFactory.serializerDeserializer(type), contentLength, a), + (is, a) -> deserializeObject(toBufferPublisher(is, a), serializerFactory.serializerDeserializer(type), + contentLength, a)); } @Override public boolean isWriteable(final Class type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) { - return !isSse(requestCtxProvider.get()) && isSupportedMediaType(mediaType); } + @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 BufferAllocator allocator = ctxRefProvider.get().get().executionContext().bufferAllocator(); final Publisher bufferPublisher; if (o instanceof Single) { - bufferPublisher = getResponseBufferPublisher(((Single) o).toPublisher(), genericType, mediaType); + final Class clazz = genericType instanceof Class ? (Class) genericType : getSourceClass(genericType); + Serializer serializer = getJacksonSerializerFactory(mediaType).serializerDeserializer(clazz); + bufferPublisher = ((Single) o).map(t -> serializer.serialize(t, allocator)).toPublisher(); } else if (o instanceof Publisher) { - bufferPublisher = getResponseBufferPublisher((Publisher) o, genericType, mediaType); + final Class clazz = genericType instanceof Class ? (Class) genericType : getSourceClass(genericType); + StreamingSerializer serializer = getJacksonSerializerFactory(mediaType) + .streamingSerializerDeserializer(clazz); + bufferPublisher = serializer.serialize((Publisher) o, allocator); } else { - bufferPublisher = getResponseBufferPublisher(Publisher.from(o), o.getClass(), mediaType); + Serializer serializer = getJacksonSerializerFactory(mediaType).serializerDeserializer(o.getClass()); + bufferPublisher = Publisher.from(serializer.serialize(o, allocator)); } 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)); - } - - private Serializer getSerializer(final MediaType mediaType) { - return new DefaultSerializer(getJacksonSerializationProvider(mediaType)); - } + private JacksonSerializerFactory getJacksonSerializerFactory(final MediaType mediaType) { + final ContextResolver contextResolver = + providers.getContextResolver(JacksonSerializerFactory.class, mediaType); - private JacksonSerializationProvider getJacksonSerializationProvider(final MediaType mediaType) { - final ContextResolver contextResolver = - providers.getContextResolver(JacksonSerializationProvider.class, mediaType); - - return contextResolver != null ? contextResolver.getContext(JacksonSerializationProvider.class) : - DEFAULT_JACKSON_SERIALIZATION_PROVIDER; + return contextResolver != null ? contextResolver.getContext(JacksonSerializerFactory.class) : + JacksonSerializerFactory.JACKSON; } private static Publisher toBufferPublisher(final InputStream is, final BufferAllocator a) { return fromInputStream(is).map(a::wrap); } - private static Single deserialize(final Publisher bufferPublisher, final Serializer ser, - final Class type, final int contentLength, - final BufferAllocator allocator) { - + private static Single deserialize( + final Publisher bufferPublisher, final Deserializer deserializer, final int contentLength, + final BufferAllocator allocator) { return bufferPublisher .collect(() -> newBufferForRequestContent(contentLength, allocator), Buffer::writeBytes) .map(buf -> { try { - return ser.deserializeAggregatedSingle(buf, type); + return deserializer.deserialize(buf, allocator); } catch (final NoSuchElementException e) { throw new BadRequestException("No deserializable JSON content", e); } catch (final SerializationException e) { @@ -192,10 +185,9 @@ static Buffer newBufferForRequestContent(final int contentLength, } // visible for testing - static T deserializeObject(final Publisher bufferPublisher, final Serializer ser, - final Class type, final int contentLength, - final BufferAllocator allocator) { - return awaitResult(deserialize(bufferPublisher, ser, type, contentLength, allocator).toFuture()); + static T deserializeObject(final Publisher bufferPublisher, final Deserializer deserializer, + final int contentLength, final BufferAllocator allocator) { + return awaitResult(deserialize(bufferPublisher, deserializer, contentLength, allocator).toFuture()); } private static boolean isSse(ContainerRequestContext requestCtx) { diff --git a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/SerializationExceptionMapper.java b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/SerializationExceptionMapper.java index d230d466bb..a6ab08e0fe 100644 --- a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/SerializationExceptionMapper.java +++ b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/SerializationExceptionMapper.java @@ -15,7 +15,7 @@ */ package io.servicetalk.data.jackson.jersey; -import io.servicetalk.serialization.api.SerializationException; +import io.servicetalk.serializer.api.SerializationException; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -23,14 +23,14 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; import static javax.ws.rs.core.Response.status; final class SerializationExceptionMapper implements ExceptionMapper { @Override public Response toResponse(final SerializationException e) { - return status(isDueToBadUserData(e) ? BAD_REQUEST : INTERNAL_SERVER_ERROR).build(); + return status(isDueToBadUserData(e) ? UNSUPPORTED_MEDIA_TYPE : INTERNAL_SERVER_ERROR).build(); } private static boolean isDueToBadUserData(final SerializationException e) { diff --git a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/ServiceTalkJacksonSerializerFeature.java b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/ServiceTalkJacksonSerializerFeature.java index b57a157020..3e5f36235d 100644 --- a/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/ServiceTalkJacksonSerializerFeature.java +++ b/servicetalk-data-jackson-jersey/src/main/java/io/servicetalk/data/jackson/jersey/ServiceTalkJacksonSerializerFeature.java @@ -16,6 +16,7 @@ package io.servicetalk.data.jackson.jersey; import io.servicetalk.data.jackson.JacksonSerializationProvider; +import io.servicetalk.data.jackson.JacksonSerializerFactory; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; @@ -69,22 +70,42 @@ public boolean configure(final FeatureContext context) { /** * Create a new {@link ContextResolver} for {@link JacksonSerializationProvider} used by this feature. - * + * @deprecated Use {@link #newContextResolver(ObjectMapper)}. * @param objectMapper the {@link ObjectMapper} to use for creating a {@link JacksonSerializationProvider}. * @return a {@link ContextResolver}. */ + @Deprecated public static ContextResolver contextResolverFor(final ObjectMapper objectMapper) { return contextResolverFor(new JacksonSerializationProvider(objectMapper)); } /** * Create a new {@link ContextResolver} for {@link JacksonSerializationProvider} used by this feature. - * + * @deprecated Use {@link #newContextResolver(JacksonSerializerFactory)}. * @param serializationProvider the {@link JacksonSerializationProvider} to use. * @return a {@link ContextResolver}. */ + @Deprecated public static ContextResolver contextResolverFor( final JacksonSerializationProvider serializationProvider) { return new JacksonSerializationProviderContextResolver(requireNonNull(serializationProvider)); } + + /** + * Create a new {@link ContextResolver} for {@link ObjectMapper} used by this feature. + * @param objectMapper the {@link ObjectMapper} to use for creating a {@link JacksonSerializerFactory}. + * @return a {@link ContextResolver}. + */ + public static ContextResolver newContextResolver(ObjectMapper objectMapper) { + return newContextResolver(new JacksonSerializerFactory(objectMapper)); + } + + /** + * Create a new {@link ContextResolver} for {@link JacksonSerializerFactory} used by this feature. + * @param cache the {@link JacksonSerializerFactory} to use. + * @return a {@link ContextResolver}. + */ + public static ContextResolver newContextResolver(JacksonSerializerFactory cache) { + return new JacksonSerializerFactoryContextResolver(cache); + } } 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 0f7aa35eda..96ece136ef 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 @@ -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.contextResolverFor; +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; @@ -47,7 +47,7 @@ public Set> getClasses() { @Override public Set getSingletons() { - return singleton(contextResolverFor(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES))); + return singleton(newContextResolver(new ObjectMapper().disable(FAIL_ON_UNKNOWN_PROPERTIES))); } @Override diff --git a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriterTest.java b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriterTest.java index 7189e8933a..7064ac9784 100644 --- a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriterTest.java +++ b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/JacksonSerializerMessageBodyReaderWriterTest.java @@ -15,9 +15,8 @@ */ package io.servicetalk.data.jackson.jersey; -import io.servicetalk.data.jackson.JacksonSerializationProvider; -import io.servicetalk.serialization.api.DefaultSerializer; -import io.servicetalk.serialization.api.SerializationException; +import io.servicetalk.data.jackson.JacksonSerializerFactory; +import io.servicetalk.serializer.api.SerializationException; import org.junit.jupiter.api.Test; @@ -38,7 +37,7 @@ void deserializeObjectDoesntRepeatBadRequestException() { BadRequestException ex = assertThrows(BadRequestException.class, () -> deserializeObject(from(DEFAULT_ALLOCATOR.fromAscii("{foo:123}")), - new DefaultSerializer(new JacksonSerializationProvider()), Map.class, 9, DEFAULT_ALLOCATOR)); + JacksonSerializerFactory.JACKSON.serializerDeserializer(Map.class), 9, DEFAULT_ALLOCATOR)); assertThat(ex.getCause(), instanceOf(SerializationException.class)); } } diff --git a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/SingleJsonResourcesTest.java b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/SingleJsonResourcesTest.java index 89e9c29b89..af203466ec 100644 --- a/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/SingleJsonResourcesTest.java +++ b/servicetalk-data-jackson-jersey/src/test/java/io/servicetalk/data/jackson/jersey/SingleJsonResourcesTest.java @@ -20,7 +20,7 @@ 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.BAD_REQUEST; +import static io.servicetalk.http.api.HttpResponseStatus.OK; class SingleJsonResourcesTest extends AbstractStreamingJsonResourcesTest { @@ -33,6 +33,6 @@ protected String testUri(final String path) { @EnumSource(RouterApi.class) void postTooManyJsonMaps(RouterApi api) throws Exception { setUp(api); - sendAndAssertStatusOnly(post("/map", "{\"foo1\":\"bar1\"}{\"foo2\":\"bar2\"}", APPLICATION_JSON), BAD_REQUEST); + sendAndAssertStatusOnly(post("/map", "{\"foo1\":\"bar1\"}{\"foo2\":\"bar2\"}", APPLICATION_JSON), OK); } } diff --git a/servicetalk-data-jackson/build.gradle b/servicetalk-data-jackson/build.gradle index 16d9504bac..60001b1659 100644 --- a/servicetalk-data-jackson/build.gradle +++ b/servicetalk-data-jackson/build.gradle @@ -23,8 +23,10 @@ dependencies { implementation project(":servicetalk-annotations") implementation project(":servicetalk-concurrent-api-internal") + implementation project(":servicetalk-concurrent-internal") implementation project(":servicetalk-utils-internal") implementation project(":servicetalk-serialization-api") + implementation project(":servicetalk-serializer-api") implementation "com.google.code.findbugs:jsr305:$jsr305Version" testImplementation testFixtures(project(":servicetalk-concurrent-api")) @@ -32,6 +34,7 @@ dependencies { testImplementation project(":servicetalk-test-resources") testImplementation project(":servicetalk-buffer-netty") testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5Version" testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/AbstractJacksonDeserializer.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/AbstractJacksonDeserializer.java index 851a567384..9cad941afa 100644 --- a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/AbstractJacksonDeserializer.java +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/AbstractJacksonDeserializer.java @@ -39,6 +39,7 @@ import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance; import static java.util.Collections.emptyList; +@Deprecated abstract class AbstractJacksonDeserializer implements StreamingDeserializer { private final Deque nodeStack = new ArrayDeque<>(); diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteArrayJacksonDeserializer.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteArrayJacksonDeserializer.java index 472e9a0507..afe1ea2602 100644 --- a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteArrayJacksonDeserializer.java +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteArrayJacksonDeserializer.java @@ -28,6 +28,7 @@ import static java.util.Collections.emptyList; +@Deprecated final class ByteArrayJacksonDeserializer extends AbstractJacksonDeserializer { private final ByteArrayFeeder feeder; @@ -39,8 +40,8 @@ final class ByteArrayJacksonDeserializer extends AbstractJacksonDeserializer< @Nonnull Iterable doDeserialize(final Buffer buffer, @Nullable List resultHolder) throws IOException { if (buffer.hasArray()) { - feeder.feedInput(buffer.array(), buffer.arrayOffset() + buffer.readerIndex(), - buffer.arrayOffset() + buffer.readableBytes()); + final int start = buffer.arrayOffset() + buffer.readerIndex(); + feeder.feedInput(buffer.array(), start, start + buffer.readableBytes()); } else { int readableBytes = buffer.readableBytes(); if (readableBytes != 0) { diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteBufferJacksonDeserializer.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteBufferJacksonDeserializer.java index 4760753fc4..f2a0add938 100644 --- a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteBufferJacksonDeserializer.java +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/ByteBufferJacksonDeserializer.java @@ -27,6 +27,7 @@ import static java.util.Collections.emptyList; +@Deprecated final class ByteBufferJacksonDeserializer extends AbstractJacksonDeserializer { private final ByteBufferFeeder feeder; diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializationProvider.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializationProvider.java index a96724f5e1..a8e34f37b6 100644 --- a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializationProvider.java +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializationProvider.java @@ -38,7 +38,9 @@ /** * {@link SerializationProvider} implementation using jackson. + * @deprecated Use {@link JacksonSerializerFactory}. */ +@Deprecated public final class JacksonSerializationProvider implements SerializationProvider { private final ObjectMapper mapper; diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializer.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializer.java new file mode 100644 index 0000000000..84e11c1435 --- /dev/null +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializer.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2021 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; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; + +import java.io.IOException; + +import static io.servicetalk.buffer.api.Buffer.asOutputStream; + +/** + * Serializes and deserializes to/from JSON via jackson. + * @param The type of objects to serialize. + */ +final class JacksonSerializer implements SerializerDeserializer { + private final ObjectWriter writer; + private final ObjectReader reader; + + JacksonSerializer(ObjectMapper mapper, Class clazz) { + writer = mapper.writerFor(clazz); + reader = mapper.readerFor(clazz); + } + + JacksonSerializer(ObjectMapper mapper, TypeReference typeRef) { + writer = mapper.writerFor(typeRef); + reader = mapper.readerFor(typeRef); + } + + JacksonSerializer(ObjectMapper mapper, JavaType type) { + writer = mapper.writerFor(type); + reader = mapper.readerFor(type); + } + + @Override + public void serialize(final T toSerialize, final BufferAllocator allocator, final Buffer buffer) { + doSerialize(writer, toSerialize, buffer); + } + + @Override + public T deserialize(final Buffer serializedData, final BufferAllocator allocator) { + try { + return reader.readValue(Buffer.asInputStream(serializedData)); + } catch (IOException e) { + throw new SerializationException(e); + } + } + + static void doSerialize(final ObjectWriter writer, T t, Buffer destination) { + try { + writer.writeValue(asOutputStream(destination), t); + } catch (IOException e) { + throw new SerializationException(e); + } + } +} diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializerFactory.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializerFactory.java new file mode 100644 index 0000000000..52a246dcb7 --- /dev/null +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonSerializerFactory.java @@ -0,0 +1,130 @@ +/* + * Copyright © 2021 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; + +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches instances of {@link SerializerDeserializer} and {@link StreamingSerializerDeserializer} for + * jackson. + */ +public final class JacksonSerializerFactory { + /** + * Singleton instance which creates jackson serializers. + */ + public static final JacksonSerializerFactory JACKSON = new JacksonSerializerFactory(); + private final ObjectMapper mapper; + @SuppressWarnings("rawtypes") + private final Map streamingSerializerMap; + @SuppressWarnings("rawtypes") + private final Map serializerMap; + + /** + * Create a new instance. + */ + private JacksonSerializerFactory() { + this(new ObjectMapper()); + } + + /** + * Create a new instance. + * @param mapper {@link ObjectMapper} to use. + */ + public JacksonSerializerFactory(final ObjectMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + streamingSerializerMap = new ConcurrentHashMap<>(); + serializerMap = new ConcurrentHashMap<>(); + } + + /** + * Get a {@link SerializerDeserializer}. + * @param clazz The class to serialize and deserialize. + * @param The type to serialize and deserialize. + * @return a {@link SerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public SerializerDeserializer serializerDeserializer(final Class clazz) { + return serializerMap.computeIfAbsent(clazz, clazz2 -> new JacksonSerializer<>(mapper, (Class) clazz2)); + } + + /** + * Get a {@link SerializerDeserializer}. + * @param typeRef The type reference to serialize and deserialize (captures generic type arguments at runtime). + * @param The type to serialize and deserialize. + * @return a {@link SerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public SerializerDeserializer serializerDeserializer(final TypeReference typeRef) { + return serializerMap.computeIfAbsent(typeRef, typeRef2 -> + new JacksonSerializer<>(mapper, (TypeReference) typeRef2)); + } + + /** + * Get a {@link SerializerDeserializer}. + * @param type The type to serialize and deserialize (captures generic type arguments at runtime). + * @param The type to serialize and deserialize. + * @return a {@link SerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public SerializerDeserializer serializerDeserializer(final JavaType type) { + return serializerMap.computeIfAbsent(type, type2 -> new JacksonSerializer<>(mapper, (JavaType) type2)); + } + + /** + * Get a {@link StreamingSerializerDeserializer}. + * @param clazz The class to serialize and deserialize. + * @param The type to serialize and deserialize. + * @return a {@link StreamingSerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public StreamingSerializerDeserializer streamingSerializerDeserializer(final Class clazz) { + return streamingSerializerMap.computeIfAbsent(clazz, clazz2 -> + new JacksonStreamingSerializer<>(mapper, (Class) clazz2)); + } + + /** + * Get a {@link StreamingSerializerDeserializer}. + * @param typeRef The type reference to serialize and deserialize (captures generic type arguments at runtime). + * @param The type to serialize and deserialize. + * @return a {@link StreamingSerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public StreamingSerializerDeserializer streamingSerializerDeserializer(final TypeReference typeRef) { + return streamingSerializerMap.computeIfAbsent(typeRef, typeRef2 -> + new JacksonStreamingSerializer<>(mapper, (TypeReference) typeRef2)); + } + + /** + * Get a {@link StreamingSerializerDeserializer}. + * @param type The type to serialize and deserialize (captures generic type arguments at runtime). + * @param The type to serialize and deserialize. + * @return a {@link StreamingSerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public StreamingSerializerDeserializer streamingSerializerDeserializer(final JavaType type) { + return streamingSerializerMap.computeIfAbsent(type, type2 -> + new JacksonStreamingSerializer<>(mapper, (JavaType) type2)); + } +} diff --git a/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonStreamingSerializer.java b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonStreamingSerializer.java new file mode 100644 index 0000000000..b9f4b4b087 --- /dev/null +++ b/servicetalk-data-jackson/src/main/java/io/servicetalk/data/jackson/JacksonStreamingSerializer.java @@ -0,0 +1,412 @@ +/* + * Copyright © 2021 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; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.PublisherSource.Subscriber; +import io.servicetalk.concurrent.PublisherSource.Subscription; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.concurrent.api.PublisherOperator; +import io.servicetalk.concurrent.internal.ConcurrentSubscription; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.async.ByteArrayFeeder; +import com.fasterxml.jackson.core.async.ByteBufferFeeder; +import com.fasterxml.jackson.core.async.NonBlockingInputFeeder; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import javax.annotation.Nullable; + +import static com.fasterxml.jackson.databind.node.JsonNodeFactory.instance; +import static io.servicetalk.concurrent.internal.EmptySubscriptions.EMPTY_SUBSCRIPTION; +import static io.servicetalk.data.jackson.JacksonSerializer.doSerialize; +import static java.util.Collections.singletonList; +import static java.util.function.Function.identity; + +/** + * Serialize and deserialize a stream of JSON objects. + * @param The type of objects to serialize. + */ +final class JacksonStreamingSerializer implements StreamingSerializerDeserializer { + private final ObjectWriter writer; + private final ObjectReader reader; + + JacksonStreamingSerializer(ObjectMapper mapper, Class clazz) { + writer = mapper.writerFor(clazz); + reader = mapper.readerFor(clazz); + } + + JacksonStreamingSerializer(ObjectMapper mapper, TypeReference typeRef) { + writer = mapper.writerFor(typeRef); + reader = mapper.readerFor(typeRef); + } + + JacksonStreamingSerializer(ObjectMapper mapper, JavaType type) { + writer = mapper.writerFor(type); + reader = mapper.readerFor(type); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize.map(t -> { + Buffer buffer = allocator.newBuffer(); + doSerialize(writer, t, buffer); + return buffer; + }); + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return serializedData.liftSync(new DeserializeOperator(reader)).flatMapConcatIterable(identity()); + } + + private static final class DeserializeOperator implements PublisherOperator> { + private final ObjectReader reader; + + private DeserializeOperator(ObjectReader reader) { + this.reader = reader; + } + + @Override + public Subscriber apply(final Subscriber> subscriber) { + final JsonParser parser; + try { + // TODO(scott): ByteBufferFeeder is currently not supported by jackson, and the current API throws + // UnsupportedOperationException if not supported. When jackson does support two NonBlockingInputFeeder + // types we need an approach which doesn't involve catching UnsupportedOperationException to try to get + // ByteBufferFeeder and then ByteArrayFeeder. + parser = reader.getFactory().createNonBlockingByteArrayParser(); + } catch (IOException e) { + throw new SerializationException(e); + } + NonBlockingInputFeeder feeder = parser.getNonBlockingInputFeeder(); + if (feeder instanceof ByteBufferFeeder) { + return new ByteBufferDeserializeSubscriber<>(subscriber, reader, parser, (ByteBufferFeeder) feeder); + } else if (feeder instanceof ByteArrayFeeder) { + return new ByteArrayDeserializeSubscriber<>(subscriber, reader, parser, (ByteArrayFeeder) feeder); + } + return new FailedSubscriber<>(subscriber, new SerializationException("unsupported feeder type: " + feeder)); + } + + private static final class ByteArrayDeserializeSubscriber extends DeserializeSubscriber { + private final ByteArrayFeeder feeder; + + private ByteArrayDeserializeSubscriber(final Subscriber> subscriber, + final ObjectReader reader, final JsonParser parser, + final ByteArrayFeeder feeder) { + super(subscriber, reader, parser); + this.feeder = feeder; + } + + @Override + boolean consumeOnNext(final Buffer buffer) throws IOException { + if (buffer.hasArray()) { + final int start = buffer.arrayOffset() + buffer.readerIndex(); + feeder.feedInput(buffer.array(), start, start + buffer.readableBytes()); + } else { + int readableBytes = buffer.readableBytes(); + if (readableBytes != 0) { + byte[] copy = new byte[readableBytes]; + buffer.readBytes(copy); + feeder.feedInput(copy, 0, copy.length); + } + } + return feeder.needMoreInput(); + } + } + + private static final class ByteBufferDeserializeSubscriber extends DeserializeSubscriber { + private final ByteBufferFeeder feeder; + + private ByteBufferDeserializeSubscriber(final Subscriber> subscriber, + final ObjectReader reader, final JsonParser parser, + final ByteBufferFeeder feeder) { + super(subscriber, reader, parser); + this.feeder = feeder; + } + + @Override + boolean consumeOnNext(final Buffer buffer) throws IOException { + feeder.feedInput(buffer.toNioBuffer()); + return feeder.needMoreInput(); + } + } + + private abstract static class DeserializeSubscriber implements Subscriber { + private final JsonParser parser; + private final ObjectReader reader; + private final Deque tokenStack = new ArrayDeque<>(8); + private final Subscriber> subscriber; + @Nullable + private Subscription subscription; + @Nullable + private String fieldName; + + private DeserializeSubscriber(final Subscriber> subscriber, + final ObjectReader reader, + final JsonParser parser) { + this.reader = reader; + this.parser = parser; + this.subscriber = subscriber; + } + + /** + * Consumer the buffer from {@link #onNext(Buffer)}. + * @param buffer The bytes to append. + * @return {@code true} if more data is required to parse an object. {@code false} if object(s) should be + * parsed after this method returns. + * @throws IOException If an exception occurs while appending {@link Buffer}. + */ + abstract boolean consumeOnNext(Buffer buffer) throws IOException; + + @Override + public final void onSubscribe(final Subscription subscription) { + this.subscription = ConcurrentSubscription.wrap(subscription); + subscriber.onSubscribe(this.subscription); + } + + @Override + public final void onNext(@Nullable final Buffer buffer) { + assert subscription != null; + try { + if (buffer == null || consumeOnNext(buffer)) { + subscription.request(1); + } else { + JsonToken token; + List values = null; + T value = null; + while ((token = parser.nextToken()) != JsonToken.NOT_AVAILABLE) { + JsonNode nextRoot = push(token, parser); + if (nextRoot != null) { + if (values != null) { + values.add(reader.readValue(nextRoot)); + } else if (value == null) { + value = reader.readValue(nextRoot); + } else { + values = new ArrayList<>(3); + values.add(value); + value = null; + values.add(reader.readValue(nextRoot)); + } + } + } + + if (values != null) { + subscriber.onNext(values); + } else if (value != null) { + subscriber.onNext(singletonList(value)); + } else { + subscription.request(1); + } + } + } catch (IOException e) { + throw new SerializationException(e); + } + } + + @Override + public final void onError(final Throwable t) { + subscriber.onError(t); + } + + @Override + public final void onComplete() { + if (tokenStack.isEmpty()) { + subscriber.onComplete(); + } else { + subscriber.onError(new SerializationException("completed with " + tokenStack.size() + + " tokens pending")); + } + } + + @Nullable + private JsonNode push(JsonToken event, JsonParser parser) throws IOException { + switch (event) { + case START_OBJECT: + tokenStack.push(createObject(tokenStack.peek())); + return null; + case START_ARRAY: + tokenStack.push(createArray(tokenStack.peek())); + return null; + case END_OBJECT: + case END_ARRAY: + JsonNode top = tokenStack.pop(); + return tokenStack.isEmpty() ? top : null; + case FIELD_NAME: + assert !tokenStack.isEmpty(); + fieldName = parser.getCurrentName(); + return null; + case VALUE_STRING: + if (tokenStack.isEmpty()) { + return new TextNode(parser.getValueAsString()); + } + addValue(tokenStack.peek(), parser.getValueAsString()); + return null; + case VALUE_NUMBER_INT: + // Ideally we want to make sure that if we deserialize a single primitive value, that is the + // only thing that this deserializer deserializes, i.e. any subsequent deserialization attempts + // MUST throw. However, to achieve that we need to maintain state between two deserialize calls. + // Jackson does not support deserializing a single primitive number as yet when used with + // non-blocking parser. Hence, we avoid doing that state management yet. + addValue(peekNonNull(), parser.getLongValue()); + return null; + case VALUE_NUMBER_FLOAT: + addValue(peekNonNull(), parser.getDoubleValue()); + return null; + case VALUE_TRUE: + if (tokenStack.isEmpty()) { + return BooleanNode.TRUE; + } + addValue(tokenStack.peek(), true); + return null; + case VALUE_FALSE: + if (tokenStack.isEmpty()) { + return BooleanNode.FALSE; + } + addValue(tokenStack.peek(), false); + return null; + case VALUE_NULL: + if (tokenStack.isEmpty()) { + return NullNode.getInstance(); + } + addNull(tokenStack.peek()); + return null; + default: + throw new IllegalArgumentException("unsupported event: " + event); + } + } + + private JsonNode peekNonNull() { + JsonNode node = tokenStack.peek(); + assert node != null; + return node; + } + + private JsonNode createObject(@Nullable JsonNode current) { + if (current instanceof ObjectNode) { + return ((ObjectNode) current).putObject(fieldName); + } else if (current instanceof ArrayNode) { + return ((ArrayNode) current).addObject(); + } else { + return instance.objectNode(); + } + } + + private JsonNode createArray(@Nullable JsonNode current) { + if (current instanceof ObjectNode) { + return ((ObjectNode) current).putArray(fieldName); + } else if (current instanceof ArrayNode) { + return ((ArrayNode) current).addArray(); + } else { + return instance.arrayNode(); + } + } + + private void addValue(JsonNode current, String s) { + if (current instanceof ObjectNode) { + ((ObjectNode) current).put(fieldName, s); + } else { + ((ArrayNode) current).add(s); + } + } + + private void addValue(JsonNode current, long v) { + if (current instanceof ObjectNode) { + ((ObjectNode) current).put(fieldName, v); + } else { + ((ArrayNode) current).add(v); + } + } + + private void addValue(JsonNode current, double v) { + if (current instanceof ObjectNode) { + ((ObjectNode) current).put(fieldName, v); + } else { + ((ArrayNode) current).add(v); + } + } + + private void addValue(JsonNode current, boolean v) { + if (current instanceof ObjectNode) { + ((ObjectNode) current).put(fieldName, v); + } else { + ((ArrayNode) current).add(v); + } + } + + private void addNull(JsonNode current) { + if (current instanceof ObjectNode) { + ((ObjectNode) current).putNull(fieldName); + } else { + ((ArrayNode) current).addNull(); + } + } + } + + private static final class FailedSubscriber implements Subscriber { + private final SerializationException exception; + private final Subscriber> subscriber; + + private FailedSubscriber(final Subscriber> subscriber, + final SerializationException exception) { + this.subscriber = subscriber; + this.exception = exception; + } + + @Override + public void onSubscribe(final Subscription subscription) { + try { + subscriber.onSubscribe(EMPTY_SUBSCRIPTION); + } catch (Throwable cause) { + subscriber.onError(cause); + return; + } + subscriber.onError(exception); + } + + @Override + public void onNext(@Nullable final Buffer buffer) { + } + + @Override + public void onError(final Throwable t) { + } + + @Override + public void onComplete() { + } + } + } +} diff --git a/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializationProviderTest.java b/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializationProviderTest.java index 24641b2ada..147cb940e7 100644 --- a/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializationProviderTest.java +++ b/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializationProviderTest.java @@ -39,8 +39,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +@Deprecated class JacksonSerializationProviderTest { - private final JacksonSerializationProvider serializationProvider = new JacksonSerializationProvider(); @Test diff --git a/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializerFactoryTest.java b/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializerFactoryTest.java new file mode 100644 index 0000000000..e6146feac5 --- /dev/null +++ b/servicetalk-data-jackson/src/test/java/io/servicetalk/data/jackson/JacksonSerializerFactoryTest.java @@ -0,0 +1,286 @@ +/* + * Copyright © 2021 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; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; + +import static io.servicetalk.buffer.api.EmptyBuffer.EMPTY_BUFFER; +import static io.servicetalk.buffer.netty.BufferAllocators.PREFER_DIRECT_ALLOCATOR; +import static io.servicetalk.buffer.netty.BufferAllocators.PREFER_HEAP_ALLOCATOR; +import static io.servicetalk.concurrent.api.Publisher.from; +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; +import static java.lang.System.lineSeparator; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JacksonSerializerFactoryTest { + private static final TypeReference TEST_POJO_TYPE_REFERENCE = new TypeReference() { }; + private static final TypeReference STRING_TYPE_REFERENCE = new TypeReference() { }; + private static final TypeReference BOOLEAN_TYPE_REFERENCE = new TypeReference() { }; + private static final TypeReference INTEGER_TYPE_REFERENCE = new TypeReference() { }; + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void aggregatedSerializeDeserialize(boolean typeRef, BufferAllocator alloc) { + TestPojo expected = new TestPojo(true, (byte) -2, (short) -3, 'a', 2, 5, 3.2f, -8.5, null, new String[] {"bar"}, + null); + + assertEquals(expected, pojoSerializer(typeRef).deserialize( + pojoSerializer(typeRef).serialize(expected, alloc), alloc)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeInvalidData(boolean typeRef, BufferAllocator alloc) { + TestPojo expected = new TestPojo(true, (byte) -2, (short) -3, 'a', 2, 5, 3.2f, -8.5, null, new String[] {"bar"}, + null); + final Buffer serialized = pojoSerializer(typeRef).serialize(expected, alloc); + serialized.setByte(serialized.writerIndex() - 1, serialized.getByte(serialized.writerIndex() - 1) + 1); + + assertThrows(io.servicetalk.serializer.api.SerializationException.class, () -> + pojoSerializer(typeRef).deserialize(serialized, alloc)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeEmpty(boolean typeRef, BufferAllocator alloc) { + assertThrows(io.servicetalk.serializer.api.SerializationException.class, + () -> pojoSerializer(typeRef).deserialize(EMPTY_BUFFER, alloc)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeIncomplete(boolean typeRef, BufferAllocator alloc) { + TestPojo expected = new TestPojo(true, (byte) -2, (short) -3, 'a', 2, 5, 3.2f, -8.5, null, new String[] {"bar"}, + null); + Buffer buffer = pojoSerializer(typeRef).serialize(expected, alloc); + assertThrows(io.servicetalk.serializer.api.SerializationException.class, () -> + pojoSerializer(typeRef).deserialize(buffer.readBytes(buffer.readableBytes() - 1), alloc)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeEmptyStreaming(boolean typeRef, BufferAllocator alloc) { + assertThat(pojoStreamingSerializer(typeRef).deserialize(singletonList(EMPTY_BUFFER), alloc), + emptyIterable()); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingSerializeDeserialize(boolean typeRef, BufferAllocator alloc) { + TestPojo expected = new TestPojo(true, Byte.MAX_VALUE, Short.MAX_VALUE, Character.MAX_VALUE, Integer.MIN_VALUE, + Long.MAX_VALUE, Float.MAX_VALUE, Double.MAX_VALUE, "foo", new String[] {"bar", "baz"}, null); + + assertThat(pojoStreamingSerializer(typeRef).deserialize( + pojoStreamingSerializer(typeRef).serialize(singletonList(expected), alloc), + alloc), contains(expected)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingSerializeDeserialize2(boolean typeRef, BufferAllocator alloc) { + TestPojo expected1 = new TestPojo(true, (byte) -2, (short) -3, 'a', 2, 5, 3.2f, -8.5, null, + new String[] {"bar", "baz"}, null); + TestPojo expected2 = new TestPojo(false, (byte) 500, (short) 353, 'r', 100, 534, 33.25f, 888.5, null, + new String[] {"foo"}, expected1); + assertThat(pojoStreamingSerializer(typeRef).deserialize( + pojoStreamingSerializer(typeRef).serialize(from(expected1, expected2), alloc), alloc) + .toIterable(), contains(expected1, expected2)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingSerializeDeserialize2SingleBuffer(boolean typeRef, BufferAllocator alloc) { + TestPojo expected1 = new TestPojo(true, (byte) -2, (short) -3, 'a', 2, 5, 3.2f, -8.5, null, + new String[] {"bar", "baz"}, null); + TestPojo expected2 = new TestPojo(false, (byte) 500, (short) 353, 'r', 100, 534, 33.25f, 888.5, null, + new String[] {"foo"}, expected1); + + final Buffer buffer1 = pojoSerializer(typeRef).serialize(expected1, alloc); + final Buffer buffer2 = pojoSerializer(typeRef).serialize(expected2, alloc); + + Buffer composite = alloc.newBuffer(buffer1.readableBytes() + buffer2.readableBytes()); + composite.writeBytes(buffer1).writeBytes(buffer2); + + assertThat(pojoStreamingSerializer(typeRef).deserialize(from(composite), alloc) + .toIterable(), contains(expected1, expected2)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingSerializeDeserializeMultipleSplitBuffers(boolean typeRef, BufferAllocator alloc) { + TestPojo expected1 = new TestPojo(true, (byte) -2, (short) -3, 'a', 2, 5, 3.2f, -8.5, null, + new String[] {"bar", "baz"}, null); + TestPojo expected2 = new TestPojo(false, (byte) 500, (short) 353, 'r', 100, 534, 33.25f, 888.5, null, + new String[] {"foo"}, expected1); + + final Buffer buffer1 = pojoSerializer(typeRef).serialize(expected1, alloc); + final Buffer buffer2 = pojoSerializer(typeRef).serialize(expected2, alloc); + + int buffer1Split = ThreadLocalRandom.current().nextInt(buffer1.readableBytes()); + int buffer2Split = ThreadLocalRandom.current().nextInt(buffer2.readableBytes()); + String debugString = splitDebugString(buffer1, buffer1Split) + lineSeparator() + + splitDebugString(buffer2, buffer2Split); + try { + assertThat(pojoStreamingSerializer(typeRef).deserialize( + from(buffer1.readBytes(buffer1Split), buffer1, buffer2.readBytes(buffer2Split), buffer2), + alloc).toIterable(), contains(expected1, expected2)); + } catch (Throwable cause) { + throw new AssertionError("failed to parse split buffers: " + debugString, cause); + } + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeString(boolean typeRef, BufferAllocator alloc) { + String json = "\"x\""; + final Buffer buffer = alloc.fromAscii(json); + assertThat(stringSerializer(typeRef).deserialize(buffer, alloc), is("x")); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingDeserializeString(boolean typeRef, BufferAllocator alloc) { + String json = "\"x\""; + final Buffer buffer = alloc.fromAscii(json); + assertThat(stringStreamingSerializer(typeRef).deserialize(from(buffer), alloc).toIterable(), + contains("x")); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeStringNull(boolean typeRef, BufferAllocator alloc) { + String json = "null"; + final Buffer buffer = alloc.fromAscii(json); + assertThat(stringSerializer(typeRef).deserialize(buffer, alloc), nullValue()); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingDeserializeStringNull(boolean typeRef, BufferAllocator alloc) { + String json = "null"; + final Buffer buffer = alloc.fromAscii(json); + assertThat(stringStreamingSerializer(typeRef).deserialize(from(buffer), alloc).toIterable(), + emptyIterable()); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeBoolean(boolean typeRef, BufferAllocator alloc) { + String json = "true"; + final Buffer buffer = alloc.fromAscii(json); + assertThat(boolSerializer(typeRef).deserialize(buffer, alloc), is(true)); + } + + @Disabled("Jackson streaming currently does not support parsing only Boolean value.") + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingDeserializeBoolean(boolean typeRef, BufferAllocator alloc) { + String json = "true"; + final Buffer buffer = alloc.fromAscii(json); + assertThat(boolStreamingSerializer(typeRef).deserialize(from(buffer), alloc).toIterable(), + contains(true)); + } + + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void deserializeInteger(boolean typeRef, BufferAllocator alloc) { + String json = "1"; + final Buffer buffer = alloc.fromAscii(json); + assertThat(intSerializer(typeRef).deserialize(buffer, alloc), is(1)); + } + + @Disabled("Jackson streaming currently does not support parsing only Integer value.") + @ParameterizedTest(name = "{index}, typeRef={0}, alloc={1}") + @MethodSource("params") + void streamingDeserializeInteger(boolean typeRef, BufferAllocator alloc) { + String json = "1"; + final Buffer buffer = alloc.fromAscii(json); + assertThat(intStreamingSerializer(typeRef).deserialize(from(buffer), alloc).toIterable(), contains(1)); + } + + private static Stream params() { + return Stream.of( + Arguments.of(true, PREFER_DIRECT_ALLOCATOR), + Arguments.of(true, PREFER_HEAP_ALLOCATOR), + Arguments.of(false, PREFER_DIRECT_ALLOCATOR), + Arguments.of(false, PREFER_HEAP_ALLOCATOR)); + } + + private static String splitDebugString(Buffer buffer, int split) { + return buffer.toString(buffer.readerIndex(), split, UTF_8) + lineSeparator() + + buffer.toString(buffer.readerIndex() + split, buffer.readableBytes() - split, UTF_8); + } + + private static SerializerDeserializer pojoSerializer(boolean typeRef) { + return typeRef ? JACKSON.serializerDeserializer(TEST_POJO_TYPE_REFERENCE) : + JACKSON.serializerDeserializer(TestPojo.class); + } + + private static StreamingSerializerDeserializer pojoStreamingSerializer(boolean typeRef) { + return typeRef ? JACKSON.streamingSerializerDeserializer(TEST_POJO_TYPE_REFERENCE) : + JACKSON.streamingSerializerDeserializer(TestPojo.class); + } + + private static SerializerDeserializer stringSerializer(boolean typeRef) { + return typeRef ? JACKSON.serializerDeserializer(STRING_TYPE_REFERENCE) : + JACKSON.serializerDeserializer(String.class); + } + + private static StreamingSerializerDeserializer stringStreamingSerializer(boolean typeRef) { + return typeRef ? JACKSON.streamingSerializerDeserializer(STRING_TYPE_REFERENCE) : + JACKSON.streamingSerializerDeserializer(String.class); + } + + private static SerializerDeserializer boolSerializer(boolean typeRef) { + return typeRef ? JACKSON.serializerDeserializer(BOOLEAN_TYPE_REFERENCE) : + JACKSON.serializerDeserializer(Boolean.class); + } + + private static StreamingSerializerDeserializer boolStreamingSerializer(boolean typeRef) { + return typeRef ? JACKSON.streamingSerializerDeserializer(BOOLEAN_TYPE_REFERENCE) : + JACKSON.streamingSerializerDeserializer(Boolean.class); + } + + private static SerializerDeserializer intSerializer(boolean typeRef) { + return typeRef ? JACKSON.serializerDeserializer(INTEGER_TYPE_REFERENCE) : + JACKSON.serializerDeserializer(Integer.class); + } + + private static StreamingSerializerDeserializer intStreamingSerializer(boolean typeRef) { + return typeRef ? JACKSON.streamingSerializerDeserializer(INTEGER_TYPE_REFERENCE) : + JACKSON.streamingSerializerDeserializer(Integer.class); + } +} diff --git a/servicetalk-data-protobuf/build.gradle b/servicetalk-data-protobuf/build.gradle index f609c3aaee..e3c65db41a 100644 --- a/servicetalk-data-protobuf/build.gradle +++ b/servicetalk-data-protobuf/build.gradle @@ -28,11 +28,13 @@ ideaModule.dependsOn "generateTestProto" dependencies { api project(":servicetalk-buffer-api") api project(":servicetalk-concurrent-api") + api project(":servicetalk-serializer-api") implementation project(":servicetalk-annotations") implementation project(":servicetalk-concurrent-api-internal") implementation project(":servicetalk-concurrent-internal") implementation project(":servicetalk-serialization-api") + implementation project(":servicetalk-serializer-utils") implementation "com.google.code.findbugs:jsr305:$jsr305Version" implementation "com.google.protobuf:protobuf-java:$protobufVersion" @@ -41,6 +43,7 @@ dependencies { testImplementation project(":servicetalk-test-resources") testImplementation project(":servicetalk-buffer-netty") testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5Version" testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" diff --git a/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializationProvider.java b/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializationProvider.java index 79ce93e699..b61f254a55 100644 --- a/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializationProvider.java +++ b/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializationProvider.java @@ -46,7 +46,9 @@ * Note: This implementation assumes byte streams represent a single message. This implementation currently uses * {@code writeTo/parseFrom} and not {@code writeDelimitedTo/parseDelimitedFrom} to serialize/deserialize messages. * It cannot be used to process a stream of delimited messages on a single Buffer. + * @deprecated Use {@link ProtobufSerializerFactory}. */ +@Deprecated public final class ProtobufSerializationProvider implements SerializationProvider { private final ConcurrentMap parsers; diff --git a/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializer.java b/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializer.java new file mode 100644 index 0000000000..24570fb2c4 --- /dev/null +++ b/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializer.java @@ -0,0 +1,118 @@ +/* + * Copyright © 2021 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.protobuf; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; +import com.google.protobuf.Parser; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static com.google.protobuf.CodedOutputStream.newInstance; +import static com.google.protobuf.UnsafeByteOperations.unsafeWrap; +import static java.util.Objects.requireNonNull; + +/** + * Serializes and deserializes protocol buffer objects. + * @param The type of objects to serialize. + */ +final class ProtobufSerializer implements SerializerDeserializer { + private final Parser parser; + + /** + * Create a new instance. + * @param parser The {@link Parser} used to serialize and deserialize. + */ + ProtobufSerializer(Parser parser) { + this.parser = requireNonNull(parser); + } + + @Override + public Buffer serialize(final T toSerialize, final BufferAllocator allocator) { + Buffer buffer = allocator.newBuffer(toSerialize.getSerializedSize()); + serialize(toSerialize, allocator, buffer); + return buffer; + } + + @Override + public void serialize(final T toSerialize, final BufferAllocator allocator, final Buffer buffer) { + final int writerIdx = buffer.writerIndex(); + final int writableBytes = buffer.writableBytes(); + final CodedOutputStream out = buffer.hasArray() ? + newInstance(buffer.array(), buffer.arrayOffset() + writerIdx, writableBytes) : + newInstance(buffer.toNioBuffer(writerIdx, writableBytes)); + + try { + toSerialize.writeTo(out); + } catch (IOException e) { + throw new SerializationException(e); + } + + // Forward write index of our buffer + buffer.writerIndex(writerIdx + toSerialize.getSerializedSize()); + } + + @Override + public T deserialize(final Buffer serializedData, final BufferAllocator allocator) { + try { + final CodedInputStream in; + if (serializedData.nioBufferCount() == 1) { + in = CodedInputStream.newInstance(serializedData.toNioBuffer()); + } else { + // Aggregated payload body may consist of multiple Buffers. In this case, + // CompositeBuffer.toNioBuffer(idx, length) may return a single ByteBuffer (when requested + // length < components[0].length) or create a new ByteBuffer and copy multiple components + // into it. Later, proto parser will copy data from this temporary ByteBuffer again. + // To avoid unnecessary copying, we use newCodedInputStream(buffers, lengthOfData). + final ByteBuffer[] buffers = serializedData.toNioBuffers(); + in = buffers.length == 1 ? + CodedInputStream.newInstance(buffers[0]) : + newCodedInputStream(buffers, serializedData.readableBytes()); + } + + T result = parser.parseFrom(in); + serializedData.skipBytes(result.getSerializedSize()); + return result; + } catch (InvalidProtocolBufferException e) { + throw new SerializationException(e); + } + } + + private static CodedInputStream newCodedInputStream(final ByteBuffer[] buffers, final int lengthOfData) { + // Because we allocated a new internal ByteBuffer that will never be mutated we may just wrap it and + // enable aliasing to avoid an extra copying inside parser for a deserialized message. + final CodedInputStream in = unsafeWrap(mergeByteBuffers(buffers, lengthOfData)).newCodedInput(); + in.enableAliasing(true); + return in; + } + + private static ByteBuffer mergeByteBuffers(final ByteBuffer[] buffers, final int lengthOfData) { + final ByteBuffer merged = ByteBuffer.allocate(lengthOfData); + for (ByteBuffer buf : buffers) { + merged.put(buf); + } + merged.flip(); + return merged; + } +} diff --git a/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializerFactory.java b/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializerFactory.java new file mode 100644 index 0000000000..be7ec6127d --- /dev/null +++ b/servicetalk-data-protobuf/src/main/java/io/servicetalk/data/protobuf/ProtobufSerializerFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2021 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.protobuf; + +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; +import io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer; + +import com.google.protobuf.MessageLite; +import com.google.protobuf.Parser; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches instances of {@link SerializerDeserializer} and {@link StreamingSerializerDeserializer} for + * protocol buffer. + */ +public final class ProtobufSerializerFactory { + /** + * Singleton instance which creates protocol buffer + * serializers. + */ + public static final ProtobufSerializerFactory PROTOBUF = new ProtobufSerializerFactory(); + @SuppressWarnings("rawtypes") + private final Map, SerializerDeserializer> serializerMap = new ConcurrentHashMap<>(); + @SuppressWarnings("rawtypes") + private final Map, StreamingSerializerDeserializer> streamingSerializerMap = new ConcurrentHashMap<>(); + + private ProtobufSerializerFactory() { + } + + /** + * Get a {@link SerializerDeserializer}. + * @param parser The {@link Parser} used to serialize and deserialize. + * @param The type to serialize and deserialize. + * @return a {@link SerializerDeserializer}. + */ + @SuppressWarnings("unchecked") + public SerializerDeserializer serializerDeserializer(Parser parser) { + return serializerMap.computeIfAbsent(parser, parser2 -> new ProtobufSerializer<>((Parser) parser2)); + } + + /** + * Get a {@link StreamingSerializerDeserializer} which supports <VarInt length, value> encoding as described + * in Protobuf Streaming. + * @param parser The {@link Parser} used to serialize and deserialize. + * @param The type to serialize and deserialize. + * @return a {@link StreamingSerializerDeserializer} which supports <VarInt length, value> encoding as + * described in Protobuf Streaming. + * @see VarIntLengthStreamingSerializer + */ + @SuppressWarnings("unchecked") + public StreamingSerializerDeserializer streamingSerializerDeserializer( + Parser parser) { + return streamingSerializerMap.computeIfAbsent(parser, + parser2 -> new VarIntLengthStreamingSerializer<>(serializerDeserializer((Parser) parser2), + MessageLite::getSerializedSize)); + } +} diff --git a/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializationProviderTest.java b/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializationProviderTest.java index 42ec97bfd7..943d4e0a93 100644 --- a/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializationProviderTest.java +++ b/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializationProviderTest.java @@ -36,8 +36,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.internal.util.collections.Iterables.firstOf; +@Deprecated class ProtobufSerializationProviderTest { - private final ProtobufSerializationProvider provider = new ProtobufSerializationProvider(); private final DummyMessage testMessage = DummyMessage.newBuilder().setMessage("test").build(); diff --git a/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializerFactoryTest.java b/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializerFactoryTest.java new file mode 100644 index 0000000000..284ab1f3b5 --- /dev/null +++ b/servicetalk-data-protobuf/src/test/java/io/servicetalk/data/protobuf/ProtobufSerializerFactoryTest.java @@ -0,0 +1,119 @@ +/* + * Copyright © 2021 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.protobuf; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.data.protobuf.test.TestProtos.DummyMessage; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import com.google.protobuf.Parser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import static io.servicetalk.buffer.api.Buffer.asInputStream; +import static io.servicetalk.buffer.api.Buffer.asOutputStream; +import static io.servicetalk.buffer.netty.BufferAllocators.DEFAULT_ALLOCATOR; +import static io.servicetalk.concurrent.api.Publisher.from; +import static io.servicetalk.concurrent.api.Publisher.fromIterable; +import static io.servicetalk.data.protobuf.ProtobufSerializerFactory.PROTOBUF; +import static io.servicetalk.data.protobuf.test.TestProtos.DummyMessage.parser; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; + +class ProtobufSerializerFactoryTest { + private static final List POJOS = Arrays.asList( + Arguments.of(singletonList(newMsg("hello"))), + Arguments.of(asList(newMsg("hello"), newMsg("world"))), + Arguments.of(asList(newMsg("hello"), newMsg("world"), newMsg("!"))), + Arguments.of(asList(newMsg("hello"), newMsg(1 << 7))), + Arguments.of(asList(newMsg(1 << 14), newMsg("!"))), + Arguments.of(singletonList(newMsg(1 << 21))), + Arguments.of(singletonList(newMsg(1 << 28))) + ); + + @Test + void serializeDeserialize() { + final DummyMessage testMessage = DummyMessage.newBuilder().setMessage("test").build(); + final byte[] testMessageBytes = testMessage.toByteArray(); + SerializerDeserializer serializer = PROTOBUF.serializerDeserializer(DummyMessage.parser()); + Buffer buffer = serializer.serialize(testMessage, DEFAULT_ALLOCATOR); + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.getBytes(buffer.readerIndex(), bytes); + assertThat(bytes, equalTo(testMessageBytes)); + assertThat(serializer.deserialize(buffer, DEFAULT_ALLOCATOR), equalTo(testMessage)); + } + + @ParameterizedTest(name = "pojos={0}") + @MethodSource("pojos") + void streamingWriteDelimitedToDeserialized(Collection msgs) throws Exception { + Parser parser = DummyMessage.parser(); + StreamingSerializerDeserializer serializer = PROTOBUF.streamingSerializerDeserializer(parser); + + Buffer buffer = DEFAULT_ALLOCATOR.newBuffer(); + OutputStream os = asOutputStream(buffer); + for (DummyMessage msg : msgs) { + msg.writeDelimitedTo(os); + } + + assertThat(serializer.deserialize(from(buffer), DEFAULT_ALLOCATOR).toFuture().get(), contains(msgs.toArray())); + } + + @ParameterizedTest(name = "pojos={0}") + @MethodSource("pojos") + void streamingParseDelimitedFromSerialized(Collection msgs) throws Exception { + Parser parser = parser(); + StreamingSerializerDeserializer serializer = PROTOBUF.streamingSerializerDeserializer(parser); + + Collection serialized = serializer.serialize(fromIterable(msgs), DEFAULT_ALLOCATOR) + .toFuture().get(); + + Collection deserialized = new ArrayList<>(serialized.size()); + for (Buffer buf : serialized) { + deserialized.add(parser.parseDelimitedFrom(asInputStream(buf))); + } + assertThat(deserialized, contains(msgs.toArray())); + } + + @SuppressWarnings("unused") + private static Stream pojos() { + return POJOS.stream(); + } + + private static DummyMessage newMsg(String msg) { + return DummyMessage.newBuilder().setMessage(msg).build(); + } + + private static DummyMessage newMsg(int length) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + sb.append('a'); + } + return newMsg(sb.toString()); + } +} diff --git a/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/ContentCodecToBufferDecoder.java b/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/ContentCodecToBufferDecoder.java new file mode 100644 index 0000000000..bc817d5890 --- /dev/null +++ b/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/ContentCodecToBufferDecoder.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2021 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.encoding.api.internal; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.StreamingDeserializer; + +/** + * Convert from {@link ContentCodec} to {@link BufferDecoder}. + * @deprecated Use {@link BufferDecoder}. This type will be removed along with {@link ContentCodec}. + */ +@Deprecated +public final class ContentCodecToBufferDecoder implements BufferDecoder { + private final CharSequence name; + private final ContentCodecToDeserializer deserializer; + + /** + * Create a new instance. + * @param codec The codec to convert. + */ + public ContentCodecToBufferDecoder(final ContentCodec codec) { + this.name = codec.name(); + deserializer = new ContentCodecToDeserializer(codec); + } + + @Override + public Deserializer decoder() { + return deserializer; + } + + @Override + public StreamingDeserializer streamingDecoder() { + return deserializer; + } + + @Override + public CharSequence encodingName() { + return name; + } + + private static final class ContentCodecToDeserializer implements + Deserializer, StreamingDeserializer { + private final ContentCodec codec; + + private ContentCodecToDeserializer(final ContentCodec codec) { + this.codec = codec; + } + + @Override + public Buffer deserialize(final Buffer serializedData, final BufferAllocator allocator) { + return codec.decode(serializedData, allocator); + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return codec.decode(serializedData, allocator); + } + } +} diff --git a/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/ContentCodecToBufferEncoder.java b/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/ContentCodecToBufferEncoder.java new file mode 100644 index 0000000000..0bdbbe79a1 --- /dev/null +++ b/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/ContentCodecToBufferEncoder.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2021 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.encoding.api.internal; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.StreamingSerializer; + +/** + * Convert from {@link ContentCodec} to {@link BufferEncoder}. + * @deprecated Use {@link BufferEncoder}. This type will be removed along with {@link ContentCodec}. + */ +@Deprecated +public final class ContentCodecToBufferEncoder implements BufferEncoder { + private final CharSequence name; + private final ContentCodecToSerializer serializer; + + /** + * Create a new instance. + * @param codec The codec to convert. + */ + public ContentCodecToBufferEncoder(final ContentCodec codec) { + this.name = codec.name(); + serializer = new ContentCodecToSerializer(codec); + } + + @Override + public Serializer encoder() { + return serializer; + } + + @Override + public StreamingSerializer streamingEncoder() { + return serializer; + } + + @Override + public CharSequence encodingName() { + return name; + } + + private static final class ContentCodecToSerializer implements Serializer, StreamingSerializer { + private final ContentCodec codec; + + private ContentCodecToSerializer(final ContentCodec codec) { + this.codec = codec; + } + + @Override + public void serialize(final Buffer toSerialize, final BufferAllocator allocator, final Buffer buffer) { + buffer.writeBytes(codec.encode(toSerialize, allocator)); + } + + @Override + public Buffer serialize(Buffer toSerialize, BufferAllocator allocator) { + return codec.encode(toSerialize, allocator); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return codec.encode(toSerialize, allocator); + } + } +} diff --git a/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/HeaderUtils.java b/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/HeaderUtils.java index e574416ece..449e648cab 100644 --- a/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/HeaderUtils.java +++ b/servicetalk-encoding-api-internal/src/main/java/io/servicetalk/encoding/api/internal/HeaderUtils.java @@ -22,8 +22,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Function; import javax.annotation.Nullable; +import static io.servicetalk.buffer.api.CharSequences.regionMatches; import static io.servicetalk.buffer.api.CharSequences.split; import static io.servicetalk.encoding.api.Identity.identity; import static java.util.Collections.singletonList; @@ -33,7 +35,6 @@ * Header utilities to support encoding. */ public final class HeaderUtils { - private static final List NONE_CONTENT_ENCODING_SINGLETON = singletonList(identity()); private HeaderUtils() { @@ -48,16 +49,16 @@ private HeaderUtils() { * If no accepted encodings are present in the request then the result is always {@code null} * In all other cases, the first matching encoding (that is NOT {@link Identity#identity()}) is preferred, * otherwise {@code null} is returned. - * + * @deprecated Use {@link #negotiateAcceptedEncodingRaw(CharSequence, List, Function)}. * @param acceptEncodingHeaderValue The accept encoding header value. * @param serverSupportedEncodings The server supported codings as configured. * @return The {@link ContentCodec} that satisfies both client and server needs, * null if none found or matched to {@link Identity#identity()} */ + @Deprecated @Nullable public static ContentCodec negotiateAcceptedEncoding(@Nullable final CharSequence acceptEncodingHeaderValue, final List serverSupportedEncodings) { - // Fast path, server has no encodings configured or has only identity configured as encoding if (serverSupportedEncodings.isEmpty() || (serverSupportedEncodings.size() == 1 && serverSupportedEncodings.contains(identity()))) { @@ -69,6 +70,77 @@ public static ContentCodec negotiateAcceptedEncoding(@Nullable final CharSequenc return negotiateAcceptedEncoding(clientSupportedEncodings, serverSupportedEncodings); } + /** + * Get an encoder from {@code supportedEncoders} that is acceptable as referenced by + * {@code acceptEncodingHeaderValue}. + * @param acceptEncodingHeaderValue The accept encoding header value. + * @param supportedEncoders The supported encoders. + * @param messageEncodingFunc Accessor to get the encoder form an element of {@code supportedEncoders}. + * @param The type containing the encoder. + * @return an encoder from {@code supportedEncoders} that is acceptable as referenced by + * {@code acceptEncodingHeaderValue}. + */ + @Nullable + public static T negotiateAcceptedEncodingRaw( + @Nullable final CharSequence acceptEncodingHeaderValue, + final List supportedEncoders, + final Function messageEncodingFunc) { + // Fast path, server has no encodings configured or has only identity configured as encoding + if (acceptEncodingHeaderValue == null || supportedEncoders.isEmpty()) { + return null; + } + + int i = 0; + do { + // Find the next comma separated value. + int j = CharSequences.indexOf(acceptEncodingHeaderValue, ',', i); + if (j < 0) { + j = acceptEncodingHeaderValue.length(); + } + + if (i >= j) { + return null; + } + + // Trim spaces from end. + int jNonTrimmed = j; + while (acceptEncodingHeaderValue.charAt(j - 1) == ' ') { + if (--j == i) { + return null; + } + } + + // Trim spaces from beginning. + char firstChar; + while ((firstChar = acceptEncodingHeaderValue.charAt(i)) == ' ') { + if (++i == j) { + return null; + } + } + // Match special case '*' wild card, ignore qvalue prioritization for now. + if (firstChar == '*') { + return supportedEncoders.get(0); + } + + // If the accepted encoding is supported, use it. + int x = 0; + do { + T supportedEncoding = supportedEncoders.get(x); + CharSequence serverSupported = messageEncodingFunc.apply(supportedEncoding); + // Use serverSupported.length() as we ignore qvalue prioritization for now. + // All content-coding values are case-insensitive [1]. + // [1] https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.2.1. + if (regionMatches(acceptEncodingHeaderValue, true, i, serverSupported, 0, serverSupported.length())) { + return supportedEncoding; + } + } while (++x < supportedEncoders.size()); + + i = jNonTrimmed + 1; + } while (i < acceptEncodingHeaderValue.length()); + + return null; + } + /** * Establish a commonly accepted encoding between server and client, according to the supported-encodings * on the server side and the incoming header on the request. @@ -76,12 +148,13 @@ public static ContentCodec negotiateAcceptedEncoding(@Nullable final CharSequenc * If no supported encodings are passed then the result is always {@code null} * Otherwise, the first matching encoding (that is NOT {@link Identity#identity()}) is preferred, * or {@code null} is returned. - * + * @deprecated Use {@link #negotiateAcceptedEncodingRaw(CharSequence, List, Function)}. * @param clientSupportedEncodings The client supported codings as found in the HTTP header. * @param serverSupportedEncodings The server supported codings as configured. * @return The {@link ContentCodec} that satisfies both client and server needs, * null if none found or matched to {@link Identity#identity()} */ + @Deprecated @Nullable public static ContentCodec negotiateAcceptedEncoding(final List clientSupportedEncodings, final List serverSupportedEncodings) { @@ -100,6 +173,7 @@ public static ContentCodec negotiateAcceptedEncoding(final List cl return null; } + @Deprecated private static List parseAcceptEncoding(@Nullable final CharSequence acceptEncodingHeaderValue, final List allowedEncodings) { @@ -124,11 +198,12 @@ private static List parseAcceptEncoding(@Nullable final CharSequen * if {@code name} is {@code null} or empty it results in {@code null} . * If {@code name} is {@code 'identity'} this will always result in * {@link Identity#identity()} regardless of its presence in the {@code allowedList}. - * + * @deprecated Use {@link #encodingForRaw(List, Function, CharSequence)}. * @param allowedList the source list to find a matching codec from. * @param name the codec name used for the equality predicate. * @return a codec from the allowed-list that name matches the {@code name}. */ + @Deprecated @Nullable public static ContentCodec encodingFor(final Collection allowedList, @Nullable final CharSequence name) { @@ -152,7 +227,30 @@ public static ContentCodec encodingFor(final Collection allowedLis return null; } + /** + * Get the first encoding that matches {@code name} from {@code supportedEncoders}. + * @param supportedEncoders The {@link List} of supported encoders. + * @param messageEncodingFunc A means to access the supported encoding name from an element in + * {@code supportedEncoders}. + * @param name The encoding name. + * @param The type containing the encoder. + * @return the first encoding that matches {@code name} from {@code supportedEncoders}. + */ + @Nullable + public static T encodingForRaw(final List supportedEncoders, + final Function messageEncodingFunc, + final CharSequence name) { + for (T allowed : supportedEncoders) { + // Encoding values can potentially included compression configurations, we only match on the type + if (startsWith(name, messageEncodingFunc.apply(allowed))) { + return allowed; + } + } + + return null; + } + private static boolean startsWith(final CharSequence string, final CharSequence prefix) { - return CharSequences.regionMatches(string, true, 0, prefix, 0, prefix.length()); + return regionMatches(string, true, 0, prefix, 0, prefix.length()); } } diff --git a/servicetalk-encoding-api/build.gradle b/servicetalk-encoding-api/build.gradle index 1af39a032b..e1aa8322de 100644 --- a/servicetalk-encoding-api/build.gradle +++ b/servicetalk-encoding-api/build.gradle @@ -19,6 +19,7 @@ apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" dependencies { api project(":servicetalk-buffer-api") api project(":servicetalk-concurrent-api") + api project(":servicetalk-serializer-api") implementation project(":servicetalk-annotations") implementation project(":servicetalk-concurrent-internal") diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoder.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoder.java new file mode 100644 index 0000000000..1df810e23e --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoder.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2021 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.encoding.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.StreamingDeserializer; + +/** + * Used to decode buffers for aggregated and streaming use cases. + */ +public interface BufferDecoder { + /** + * Get the {@link Deserializer} to use for aggregated content. + * @return the {@link Deserializer} to use for aggregated content. + */ + Deserializer decoder(); + + /** + * Get the {@link StreamingDeserializer} to use for streaming content. + * @return the {@link StreamingDeserializer} to use for streaming content. + */ + StreamingDeserializer streamingDecoder(); + + /** + * Get the name of the encoding. + * @return the name of the encoding. + */ + CharSequence encodingName(); +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoderGroup.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoderGroup.java new file mode 100644 index 0000000000..97c25b8edb --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoderGroup.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2021 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.encoding.api; + +import java.util.List; +import javax.annotation.Nullable; + +/** + * A group of {@link BufferDecoder}s used when multiple options may be supported. + */ +public interface BufferDecoderGroup { + /** + * Get the supported {@link BufferDecoder} for this group. + * @return the supported {@link BufferDecoder} for this group. + */ + List decoders(); + + /** + * Get the combined encoding to advertise. This is typically a combination of + * {@link BufferDecoder#encodingName()} contained in this group. + * @return the combined encoding to advertise. This is typically a combination of + * {@link BufferDecoder#encodingName()} contained in this group. + */ + @Nullable + CharSequence advertisedMessageEncoding(); +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoderGroupBuilder.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoderGroupBuilder.java new file mode 100644 index 0000000000..2d099aad38 --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferDecoderGroupBuilder.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2021 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.encoding.api; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +import static io.servicetalk.buffer.api.CharSequences.newAsciiString; +import static java.util.Collections.emptyList; + +/** + * Builder for {@link BufferDecoderGroup}s. + */ +public final class BufferDecoderGroupBuilder { + private static final char CONTENT_ENCODING_SEPARATOR = ','; + private final StringBuilder messageEncoding; + private final List decoders; + + /** + * Create a new instance. + */ + public BufferDecoderGroupBuilder() { + this(2); + } + + /** + * Create a new instance. + * @param decodersSizeEstimate estimate as to how many {@link BufferDecoder} will be included in the + * {@link BufferDecoderGroup} built by this builder. + */ + public BufferDecoderGroupBuilder(int decodersSizeEstimate) { + messageEncoding = new StringBuilder(decodersSizeEstimate * 8); + decoders = new ArrayList<>(decodersSizeEstimate); + } + + /** + * Add a new {@link BufferDecoder} to the {@link BufferDecoderGroup} built by this builder. + * @param decoder The decoder to add. + * @param advertised {@code true} if the decoder should be included in + * {@link BufferDecoderGroup#advertisedMessageEncoding()}. + * @return {@code this}. + */ + public BufferDecoderGroupBuilder add(BufferDecoder decoder, boolean advertised) { + decoders.add(decoder); + if (advertised) { + if (messageEncoding.length() > 0) { + messageEncoding.append(CONTENT_ENCODING_SEPARATOR); + } + messageEncoding.append(decoder.encodingName()); + } + return this; + } + + /** + * Build a new {@link BufferDecoderGroup}. + * @return a new {@link BufferDecoderGroup}. + */ + public BufferDecoderGroup build() { + return new BufferDecoderGroup() { + private final List bufferEncoders = decoders.isEmpty() ? emptyList() : + new ArrayList<>(decoders); + @Nullable + private final CharSequence advertisedMessageEncoding = messageEncoding.length() == 0 ? + null : newAsciiString(messageEncoding); + + @Override + public List decoders() { + return bufferEncoders; + } + + @Nullable + @Override + public CharSequence advertisedMessageEncoding() { + return advertisedMessageEncoding; + } + }; + } +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncoder.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncoder.java new file mode 100644 index 0000000000..dd044ddbff --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncoder.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2021 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.encoding.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.StreamingSerializer; + +/** + * Used to encode buffers for aggregated and streaming use cases. + */ +public interface BufferEncoder { + /** + * Get the {@link Serializer} to use for aggregated content. + * @return the {@link Serializer} to use for aggregated content. + */ + Serializer encoder(); + + /** + * Get the {@link StreamingSerializer} to use for streaming content. + * @return the {@link StreamingSerializer} to use for streaming content. + */ + StreamingSerializer streamingEncoder(); + + /** + * Get the name of the encoding. + * @return the name of the encoding. + */ + CharSequence encodingName(); +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncoderDecoder.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncoderDecoder.java new file mode 100644 index 0000000000..9026a55dfd --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncoderDecoder.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2021 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.encoding.api; + +/** + * A {@link BufferEncoder} and {@link BufferDecoder}. + */ +public interface BufferEncoderDecoder extends BufferEncoder, BufferDecoder { +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncodingException.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncodingException.java new file mode 100644 index 0000000000..9e95dd53ce --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/BufferEncodingException.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2021 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.encoding.api; + +import io.servicetalk.serializer.api.SerializationException; + +/** + * A specialization of {@link SerializationException} used to indicate an encoding exception. + */ +public final class BufferEncodingException extends SerializationException { + private static final long serialVersionUID = -7422215018667837872L; + + /** + * Create a new instance. + * @param message The message to use. + */ + public BufferEncodingException(final String message) { + super(message); + } + + /** + * New instance. + * + * @param message for the exception. + * @param cause for this exception. + */ + public BufferEncodingException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * New instance. + * + * @param cause for this exception. + */ + public BufferEncodingException(final Throwable cause) { + super(cause); + } + + /** + * New instance. + * @param message for the exception. + * @param cause for this exception. + * @param enableSuppression whether or not suppression is enabled or disabled. + * @param writableStackTrace whether or not the stack trace should be writable. + */ + public BufferEncodingException(final String message, final Throwable cause, final boolean enableSuppression, + final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecDecodingException.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecDecodingException.java index 836859492d..379d6ac8f1 100644 --- a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecDecodingException.java +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecDecodingException.java @@ -15,10 +15,14 @@ */ package io.servicetalk.encoding.api; +import io.servicetalk.serializer.api.SerializationException; + /** * Exception thrown when something goes wrong during decoding. + * @deprecated Use {@link BufferEncodingException}. */ -public final class CodecDecodingException extends RuntimeException { +@Deprecated +public final class CodecDecodingException extends SerializationException { private static final long serialVersionUID = 5569510372715687762L; diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecEncodingException.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecEncodingException.java index 0f13401105..bc9437a71f 100644 --- a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecEncodingException.java +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/CodecEncodingException.java @@ -15,12 +15,16 @@ */ package io.servicetalk.encoding.api; +import io.servicetalk.serializer.api.SerializationException; + import static java.util.Objects.requireNonNull; /** * Exception thrown when something goes wrong during encoding. + * @deprecated Use {@link BufferEncodingException}. */ -public final class CodecEncodingException extends RuntimeException { +@Deprecated +public final class CodecEncodingException extends SerializationException { private static final long serialVersionUID = -3565785637300291924L; diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/ContentCodec.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/ContentCodec.java index d75b8b4fb5..95c1228689 100644 --- a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/ContentCodec.java +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/ContentCodec.java @@ -23,7 +23,9 @@ * API to support encode and decode of {@link Buffer}s. *

* Implementations must provide thread safety semantics, since instances could be shared across threads. + * @deprecated Use {@link BufferEncoder} and {@link BufferDecoder}. */ +@Deprecated public interface ContentCodec { /** diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/EmptyBufferDecoderGroup.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/EmptyBufferDecoderGroup.java new file mode 100644 index 0000000000..ec8c3608f8 --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/EmptyBufferDecoderGroup.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2021 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.encoding.api; + +import java.util.List; +import javax.annotation.Nullable; + +import static java.util.Collections.emptyList; + +/** + * A {@link BufferDecoderGroup} which is empty. + */ +public final class EmptyBufferDecoderGroup implements BufferDecoderGroup { + public static final BufferDecoderGroup INSTANCE = new EmptyBufferDecoderGroup(); + + private EmptyBufferDecoderGroup() { + } + + @Override + public List decoders() { + return emptyList(); + } + + @Nullable + @Override + public CharSequence advertisedMessageEncoding() { + return null; + } +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/Identity.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/Identity.java index faf1d9ad77..2270159e3d 100644 --- a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/Identity.java +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/Identity.java @@ -19,7 +19,7 @@ * Utility class that constructs and provides the default, always supported NOOP 'identity' {@link ContentCodec}. */ public final class Identity { - + @Deprecated private static final ContentCodec IDENTITY = new IdentityContentCodec(); private Identity() { @@ -28,9 +28,21 @@ private Identity() { /** * Returns the default, always supported NOOP 'identity' {@link ContentCodec}. + * @deprecated Use {@link #identityEncoder()}. * @return the default, always supported NOOP 'identity' {@link ContentCodec}. */ + @Deprecated public static ContentCodec identity() { return IDENTITY; } + + /** + * Get a {@link BufferEncoderDecoder} which provides + * "no encoding". + * @return a {@link BufferEncoderDecoder} which provides + * "no encoding". + */ + public static BufferEncoderDecoder identityEncoder() { + return IdentityBufferEncoderDecoder.INSTANCE; + } } diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityBufferEncoderDecoder.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityBufferEncoderDecoder.java new file mode 100644 index 0000000000..323be2adcb --- /dev/null +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityBufferEncoderDecoder.java @@ -0,0 +1,150 @@ +/* + * Copyright © 2021 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.encoding.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.concurrent.internal.BlockingIterables; +import io.servicetalk.oio.api.PayloadWriter; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingDeserializer; +import io.servicetalk.serializer.api.StreamingSerializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import static io.servicetalk.buffer.api.CharSequences.caseInsensitiveHashCode; +import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase; +import static io.servicetalk.buffer.api.CharSequences.newAsciiString; + +final class IdentityBufferEncoderDecoder implements BufferEncoderDecoder { + private static final CharSequence IDENTITY_NAME = newAsciiString("identity"); + private static final int HASH_CODE = caseInsensitiveHashCode(IDENTITY_NAME); + static final BufferEncoderDecoder INSTANCE = new IdentityBufferEncoderDecoder(); + + private IdentityBufferEncoderDecoder() { + } + + @Override + public Deserializer decoder() { + return NoopBufferSerializer.INSTANCE; + } + + @Override + public StreamingDeserializer streamingDecoder() { + return NoopStreamingBufferSerializer.INSTANCE; + } + + @Override + public Serializer encoder() { + return NoopBufferSerializer.INSTANCE; + } + + @Override + public StreamingSerializer streamingEncoder() { + return NoopStreamingBufferSerializer.INSTANCE; + } + + @Override + public CharSequence encodingName() { + return IDENTITY_NAME; + } + + @Override + public String toString() { + return IDENTITY_NAME.toString(); + } + + @Override + public boolean equals(Object o) { + return this == o || + o instanceof BufferEncoderDecoder && + contentEqualsIgnoreCase(encodingName(), ((BufferEncoderDecoder) o).encodingName()); + } + + @Override + public int hashCode() { + return HASH_CODE; + } + + private static final class NoopBufferSerializer implements SerializerDeserializer { + private static final SerializerDeserializer INSTANCE = new NoopBufferSerializer(); + + private NoopBufferSerializer() { + } + + @Override + public Buffer deserialize(final Buffer serializedData, final BufferAllocator allocator) { + return serializedData; + } + + @Override + public void serialize(final Buffer toSerialize, final BufferAllocator allocator, final Buffer buffer) { + if (toSerialize != buffer) { + buffer.writeBytes(toSerialize); + } + } + + @Override + public Buffer serialize(Buffer toSerialize, BufferAllocator allocator) { + return toSerialize; + } + + @Override + public String toString() { + return IDENTITY_NAME.toString(); + } + } + + private static final class NoopStreamingBufferSerializer implements StreamingSerializerDeserializer { + private static final StreamingSerializerDeserializer INSTANCE = new NoopStreamingBufferSerializer(); + private NoopStreamingBufferSerializer() { + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return serializedData; + } + + @Override + public BlockingIterable deserialize(final Iterable serializedData, + final BufferAllocator allocator) { + return BlockingIterables.from(serializedData); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize; + } + + @Override + public BlockingIterable serialize(final Iterable toSerialize, final BufferAllocator allocator) { + return BlockingIterables.from(toSerialize); + } + + @Override + public PayloadWriter serialize(final PayloadWriter writer, final BufferAllocator allocator) { + return writer; + } + + @Override + public String toString() { + return IDENTITY_NAME.toString(); + } + } +} diff --git a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityContentCodec.java b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityContentCodec.java index 0a5faa10f0..3eada01952 100644 --- a/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityContentCodec.java +++ b/servicetalk-encoding-api/src/main/java/io/servicetalk/encoding/api/IdentityContentCodec.java @@ -25,7 +25,9 @@ /** * Default, always supported NOOP 'identity' {@link ContentCodec}. + * @deprecated Will be removed along with {@link ContentCodec}. */ +@Deprecated final class IdentityContentCodec implements ContentCodec { private static final CharSequence NAME = newAsciiString("identity"); diff --git a/servicetalk-encoding-api/src/test/java/io/servicetalk/encoding/api/NoopContentCodec.java b/servicetalk-encoding-api/src/test/java/io/servicetalk/encoding/api/NoopContentCodec.java index 969cce6f30..4d675ac0a3 100644 --- a/servicetalk-encoding-api/src/test/java/io/servicetalk/encoding/api/NoopContentCodec.java +++ b/servicetalk-encoding-api/src/test/java/io/servicetalk/encoding/api/NoopContentCodec.java @@ -22,7 +22,9 @@ /** * Implementation of {@link ContentCodec} that doesn't modify the source {@link Buffer}. + * @deprecated Will be removed along with {@link ContentCodec}. */ +@Deprecated class NoopContentCodec implements ContentCodec { private final CharSequence name; diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/AbstractContentCodec.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/AbstractContentCodec.java index 278b610b0f..25414cd464 100644 --- a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/AbstractContentCodec.java +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/AbstractContentCodec.java @@ -20,6 +20,7 @@ import static io.servicetalk.buffer.api.CharSequences.caseInsensitiveHashCode; import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase; +@Deprecated abstract class AbstractContentCodec implements ContentCodec { private final CharSequence name; diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ContentCodings.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ContentCodings.java index 2f68fc1753..6ea9b2f712 100644 --- a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ContentCodings.java +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ContentCodings.java @@ -19,7 +19,9 @@ /** * Common available encoding implementations. + * @deprecated Use {@link NettyCompression} and {@link NettyBufferEncoders}. */ +@Deprecated public final class ContentCodings { private static final ContentCodec DEFAULT_GZIP = gzip().build(); diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/DefaultBufferEncoderDecoder.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/DefaultBufferEncoderDecoder.java new file mode 100644 index 0000000000..cc163005a4 --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/DefaultBufferEncoderDecoder.java @@ -0,0 +1,85 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.encoding.api.BufferEncoderDecoder; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingDeserializer; +import io.servicetalk.serializer.api.StreamingSerializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import static io.servicetalk.buffer.api.CharSequences.caseInsensitiveHashCode; +import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase; +import static java.util.Objects.requireNonNull; + +final class DefaultBufferEncoderDecoder implements BufferEncoderDecoder { + private final SerializerDeserializer compressor; + private final StreamingSerializerDeserializer streamingCompressor; + private final CharSequence encodingName; + + DefaultBufferEncoderDecoder(SerializerDeserializer compressor, + StreamingSerializerDeserializer streamingCompressor, + CharSequence encodingName) { + this.compressor = requireNonNull(compressor); + this.streamingCompressor = requireNonNull(streamingCompressor); + this.encodingName = requireNonNull(encodingName); + } + + @Override + public Serializer encoder() { + return compressor; + } + + @Override + public StreamingSerializer streamingEncoder() { + return streamingCompressor; + } + + @Override + public Deserializer decoder() { + return compressor; + } + + @Override + public StreamingDeserializer streamingDecoder() { + return streamingCompressor; + } + + @Override + public CharSequence encodingName() { + return encodingName; + } + + @Override + public boolean equals(final Object o) { + return this == o || + o instanceof DefaultBufferEncoderDecoder && + contentEqualsIgnoreCase(encodingName(), ((DefaultBufferEncoderDecoder) o).encodingName()); + } + + @Override + public int hashCode() { + return caseInsensitiveHashCode(encodingName); + } + + @Override + public String toString() { + return encodingName.toString(); + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/DeflateCompressionBuilder.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/DeflateCompressionBuilder.java new file mode 100644 index 0000000000..695f9c119b --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/DeflateCompressionBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import io.netty.handler.codec.compression.JdkZlibDecoder; +import io.netty.handler.codec.compression.JdkZlibEncoder; +import io.netty.handler.codec.compression.ZlibWrapper; + +final class DeflateCompressionBuilder extends ZipCompressionBuilder { + @Override + public SerializerDeserializer build() { + return new NettyCompressionSerializer( + () -> new JdkZlibEncoder(ZlibWrapper.ZLIB, compressionLevel()), + () -> new JdkZlibDecoder(ZlibWrapper.ZLIB, maxChunkSize())); + } + + @Override + public StreamingSerializerDeserializer buildStreaming() { + return new NettyCompressionStreamingSerializer( + () -> new JdkZlibEncoder(ZlibWrapper.ZLIB, compressionLevel()), + () -> new JdkZlibDecoder(ZlibWrapper.ZLIB, maxChunkSize()) + ); + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/GzipCompressionBuilder.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/GzipCompressionBuilder.java new file mode 100644 index 0000000000..fe5d66218d --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/GzipCompressionBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import io.netty.handler.codec.compression.JdkZlibDecoder; +import io.netty.handler.codec.compression.JdkZlibEncoder; +import io.netty.handler.codec.compression.ZlibWrapper; + +final class GzipCompressionBuilder extends ZipCompressionBuilder { + @Override + public SerializerDeserializer build() { + return new NettyCompressionSerializer( + () -> new JdkZlibEncoder(ZlibWrapper.GZIP, compressionLevel()), + () -> new JdkZlibDecoder(ZlibWrapper.GZIP, maxChunkSize())); + } + + @Override + public StreamingSerializerDeserializer buildStreaming() { + return new NettyCompressionStreamingSerializer( + () -> new JdkZlibEncoder(ZlibWrapper.GZIP, compressionLevel()), + () -> new JdkZlibDecoder(ZlibWrapper.GZIP, maxChunkSize()) + ); + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyBufferEncoders.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyBufferEncoders.java new file mode 100644 index 0000000000..5fa6b3012e --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyBufferEncoders.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.encoding.api.BufferEncoderDecoder; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import static io.servicetalk.buffer.api.CharSequences.newAsciiString; +import static io.servicetalk.encoding.netty.NettyCompression.deflateDefaultStreaming; +import static io.servicetalk.encoding.netty.NettyCompression.gzipDefaultStreaming; + +/** + * Factory methods for common {@link BufferEncoderDecoder}s. + */ +public final class NettyBufferEncoders { + private static final CharSequence GZIP = newAsciiString("gzip"); + private static final CharSequence DEFLATE = newAsciiString("deflate"); + private static final BufferEncoderDecoder DEFAULT_GZIP = bufferEncoder(NettyCompression.gzipDefault(), + gzipDefaultStreaming(), GZIP); + private static final BufferEncoderDecoder DEFAULT_DEFLATE = + bufferEncoder(NettyCompression.deflateDefault(), deflateDefaultStreaming(), DEFLATE); + + private NettyBufferEncoders() { + } + + /** + * Get a default {@link BufferEncoderDecoder} for gzip encoding. + * @return a default {@link BufferEncoderDecoder} for gzip encoding. + */ + public static BufferEncoderDecoder gzipDefault() { + return DEFAULT_GZIP; + } + + /** + * Get a default {@link BufferEncoderDecoder} for deflate encoding. + * @return a default {@link BufferEncoderDecoder} for deflate encoding. + */ + public static BufferEncoderDecoder deflateDefault() { + return DEFAULT_DEFLATE; + } + + /** + * Create a {@link BufferEncoderDecoder} given the underlying {@link SerializerDeserializer} and + * {@link StreamingSerializerDeserializer} implementations. + * @param compressor Used to provided serialization for aggregated content. + * @param streamingCompressor Used to provide serialization for stresaming content. + * @param encodingName The name of the encoding. + * @return a {@link BufferEncoderDecoder} given the underlying {@link SerializerDeserializer} and + * {@link StreamingSerializerDeserializer} implementations. + */ + public static BufferEncoderDecoder bufferEncoder(SerializerDeserializer compressor, + StreamingSerializerDeserializer streamingCompressor, + CharSequence encodingName) { + return new DefaultBufferEncoderDecoder(compressor, streamingCompressor, encodingName); + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyChannelContentCodec.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyChannelContentCodec.java index 6ca0f81177..19bbad6c4b 100644 --- a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyChannelContentCodec.java +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyChannelContentCodec.java @@ -45,6 +45,7 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static java.util.Objects.requireNonNull; +@Deprecated final class NettyChannelContentCodec extends AbstractContentCodec { private static final Logger LOGGER = LoggerFactory.getLogger(NettyChannelContentCodec.class); diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompression.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompression.java new file mode 100644 index 0000000000..b497f8f6a0 --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompression.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +/** + * Common available compression implementations. + */ +public final class NettyCompression { + private static final SerializerDeserializer DEFAULT_GZIP = gzip().build(); + private static final SerializerDeserializer DEFAULT_DEFLATE = deflate().build(); + private static final StreamingSerializerDeserializer DEFAULT_STREAM_GZIP = gzip().buildStreaming(); + private static final StreamingSerializerDeserializer DEFAULT_STREAM_DEFLATE = deflate().buildStreaming(); + + private NettyCompression() { + } + + /** + * Returns the default GZIP based {@link StreamingSerializerDeserializer}. + * @return default GZIP based {@link StreamingSerializerDeserializer} + */ + public static SerializerDeserializer gzipDefault() { + return DEFAULT_GZIP; + } + + /** + * Returns the default GZIP based {@link StreamingSerializerDeserializer}. + * @return default GZIP based {@link StreamingSerializerDeserializer} + */ + public static StreamingSerializerDeserializer gzipDefaultStreaming() { + return DEFAULT_STREAM_GZIP; + } + + /** + * Returns a GZIP based {@link ZipCompressionBuilder}. + * @return a GZIP based {@link ZipCompressionBuilder}. + */ + public static ZipCompressionBuilder gzip() { + return new GzipCompressionBuilder(); + } + + /** + * Returns the default DEFLATE based {@link SerializerDeserializer}. + * @return default DEFLATE based {@link SerializerDeserializer} + */ + public static SerializerDeserializer deflateDefault() { + return DEFAULT_DEFLATE; + } + + /** + * Returns the default DEFLATE based {@link StreamingSerializerDeserializer}. + * @return default DEFLATE based {@link StreamingSerializerDeserializer} + */ + public static StreamingSerializerDeserializer deflateDefaultStreaming() { + return DEFAULT_STREAM_DEFLATE; + } + + /** + * Returns a DEFLATE based {@link ZipCompressionBuilder}. + * @return a DEFLATE based {@link ZipCompressionBuilder}. + */ + public static ZipCompressionBuilder deflate() { + return new DeflateCompressionBuilder(); + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompressionSerializer.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompressionSerializer.java new file mode 100644 index 0000000000..1666ee96e6 --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompressionSerializer.java @@ -0,0 +1,141 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.encoding.api.BufferEncodingException; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Queue; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +import static io.netty.util.internal.PlatformDependent.throwException; +import static io.servicetalk.buffer.netty.BufferUtils.extractByteBufOrCreate; +import static io.servicetalk.buffer.netty.BufferUtils.getByteBufAllocator; +import static io.servicetalk.buffer.netty.BufferUtils.toByteBuf; +import static java.util.Objects.requireNonNull; + +final class NettyCompressionSerializer implements SerializerDeserializer { + private static final Logger LOGGER = LoggerFactory.getLogger(NettyCompressionSerializer.class); + private final Supplier> encoderSupplier; + private final Supplier decoderSupplier; + + NettyCompressionSerializer(final Supplier> encoderSupplier, + final Supplier decoderSupplier) { + this.encoderSupplier = requireNonNull(encoderSupplier); + this.decoderSupplier = requireNonNull(decoderSupplier); + } + + @Override + public void serialize(final Buffer toSerialize, final BufferAllocator allocator, final Buffer buffer) { + final ByteBuf nettyDst = toByteBuf(buffer); + final MessageToByteEncoder encoder = encoderSupplier.get(); + final EmbeddedChannel channel = newEmbeddedChannel(encoder, allocator); + try { + channel.writeOutbound(extractByteBufOrCreate(toSerialize)); + toSerialize.skipBytes(toSerialize.readableBytes()); + + // May produce footer + preparePendingData(channel); + drainChannelQueueToSingleBuffer(channel.outboundMessages(), nettyDst); + // no need to advance writerIndex -> NettyBuffer's writerIndex reflects the underlying ByteBuf value. + cleanup(channel); + } catch (Throwable e) { + safeCleanup(channel); + throw new BufferEncodingException("Unexpected exception during encoding", e); + } + } + + @Override + public Buffer serialize(final Buffer toSerialize, final BufferAllocator allocator) { + Buffer buffer = allocator.newBuffer(toSerialize.readableBytes()); + serialize(toSerialize, allocator, buffer); + return buffer; + } + + @Override + public Buffer deserialize(final Buffer serializedData, final BufferAllocator allocator) { + final Buffer buffer = allocator.newBuffer(serializedData.readableBytes()); + final ByteBuf nettyDst = toByteBuf(buffer); + final ByteToMessageDecoder decoder = decoderSupplier.get(); + final EmbeddedChannel channel = newEmbeddedChannel(decoder, allocator); + try { + channel.writeInbound(toByteBuf(serializedData)); + serializedData.skipBytes(serializedData.readableBytes()); + + drainChannelQueueToSingleBuffer(channel.inboundMessages(), nettyDst); + // no need to advance writerIndex -> NettyBuffer's writerIndex reflects the underlying ByteBuf value. + cleanup(channel); + return buffer; + } catch (Throwable e) { + safeCleanup(channel); + throw new BufferEncodingException("Unexpected exception during decoding", e); + } + } + + @Nullable + static void drainChannelQueueToSingleBuffer(final Queue queue, final ByteBuf nettyDst) { + ByteBuf buf; + while ((buf = (ByteBuf) queue.poll()) != null) { + try { + nettyDst.writeBytes(buf); + } finally { + buf.release(); + } + } + } + + private static EmbeddedChannel newEmbeddedChannel(final ChannelHandler handler, final BufferAllocator allocator) { + final EmbeddedChannel channel = new EmbeddedChannel(handler); + channel.config().setAllocator(getByteBufAllocator(allocator)); + return channel; + } + + static void preparePendingData(final EmbeddedChannel channel) { + try { + channel.close().syncUninterruptibly().get(); + channel.checkException(); + } catch (InterruptedException | ExecutionException ex) { + throwException(ex); + } + } + + static void cleanup(final EmbeddedChannel channel) { + boolean wasNotEmpty = channel.finishAndReleaseAll(); + assert !wasNotEmpty; + } + + static void safeCleanup(final EmbeddedChannel channel) { + try { + cleanup(channel); + } catch (AssertionError error) { + throw error; + } catch (Throwable t) { + LOGGER.debug("Error while closing embedded channel", t); + } + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompressionStreamingSerializer.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompressionStreamingSerializer.java new file mode 100644 index 0000000000..2b23601a8e --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/NettyCompressionStreamingSerializer.java @@ -0,0 +1,224 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.buffer.api.CompositeBuffer; +import io.servicetalk.concurrent.PublisherSource.Subscriber; +import io.servicetalk.concurrent.PublisherSource.Subscription; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.concurrent.internal.ConcurrentSubscription; +import io.servicetalk.encoding.api.BufferEncodingException; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; + +import java.util.Queue; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +import static io.servicetalk.buffer.api.ReadOnlyBufferAllocators.DEFAULT_RO_ALLOCATOR; +import static io.servicetalk.buffer.netty.BufferUtils.extractByteBufOrCreate; +import static io.servicetalk.buffer.netty.BufferUtils.getByteBufAllocator; +import static io.servicetalk.buffer.netty.BufferUtils.newBufferFrom; +import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.encoding.netty.NettyCompressionSerializer.cleanup; +import static io.servicetalk.encoding.netty.NettyCompressionSerializer.preparePendingData; +import static io.servicetalk.encoding.netty.NettyCompressionSerializer.safeCleanup; +import static java.util.Objects.requireNonNull; + +final class NettyCompressionStreamingSerializer implements StreamingSerializerDeserializer { + private static final Buffer END_OF_STREAM = DEFAULT_RO_ALLOCATOR.fromAscii(" "); + private static final int MAX_SIZE_FOR_MERGED_BUFFER = 1 << 16; + private final Supplier> encoderSupplier; + private final Supplier decoderSupplier; + + NettyCompressionStreamingSerializer(final Supplier> encoderSupplier, + final Supplier decoderSupplier) { + this.encoderSupplier = requireNonNull(encoderSupplier); + this.decoderSupplier = requireNonNull(decoderSupplier); + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return serializedData.liftSync(subscriber -> new Subscriber() { + private final ByteToMessageDecoder decoder = decoderSupplier.get(); + private final EmbeddedChannel channel = newEmbeddedChannel(decoder, allocator); + @Nullable + private Subscription subscription; + + @Override + public void onSubscribe(final Subscription subscription) { + this.subscription = ConcurrentSubscription.wrap(subscription); + subscriber.onSubscribe(this.subscription); + } + + @Override + public void onNext(@Nullable final Buffer next) { + assert subscription != null; + if (next == null) { + subscriber.onNext(null); + return; + } + + try { // onNext will produce AT-MOST N items (as received) + channel.writeInbound(extractByteBufOrCreate(next)); + next.skipBytes(next.readableBytes()); + Buffer buffer = drainChannelQueueToSingleBuffer(channel.inboundMessages(), allocator); + if (buffer != null && buffer.readableBytes() > 0) { + subscriber.onNext(buffer); + } else { // Not enough data to decompress, ask for more + subscription.request(1); + } + } catch (Throwable t) { + throw new BufferEncodingException("Unexpected exception during decoding", t); + } + } + + @Override + public void onError(final Throwable t) { + safeCleanup(channel); + subscriber.onError(t); + } + + @Override + public void onComplete() { + try { + cleanup(channel); + } catch (Throwable t) { + subscriber.onError(new BufferEncodingException("Unexpected exception during decoding", t)); + return; + } + + subscriber.onComplete(); + } + }); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize + .concat(succeeded(END_OF_STREAM)) + .liftSync(subscriber -> new Subscriber() { + private final MessageToByteEncoder encoder = encoderSupplier.get(); + private final EmbeddedChannel channel = newEmbeddedChannel(encoder, allocator); + @Nullable + private Subscription subscription; + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = ConcurrentSubscription.wrap(subscription); + subscriber.onSubscribe(this.subscription); + } + + @Override + public void onNext(@Nullable Buffer next) { + assert subscription != null; + if (next == null) { + subscriber.onNext(null); + return; + } + + try { + // onNext will produce AT-MOST N items (from upstream) + // +1 for the encoding footer (ie. END_OF_STREAM) + if (next == END_OF_STREAM) { + // May produce footer + preparePendingData(channel); + Buffer buffer = drainChannelQueueToSingleBuffer(channel.outboundMessages(), allocator); + if (buffer != null) { + subscriber.onNext(buffer); + } + } else { + channel.writeOutbound(extractByteBufOrCreate(next)); + next.skipBytes(next.readableBytes()); + Buffer buffer = drainChannelQueueToSingleBuffer(channel.outboundMessages(), allocator); + if (buffer != null && buffer.readableBytes() > 0) { + subscriber.onNext(buffer); + } else { + subscription.request(1); + } + } + } catch (Throwable t) { + throw new BufferEncodingException("Unexpected exception during encoding", t); + } + } + + @Override + public void onError(Throwable t) { + safeCleanup(channel); + subscriber.onError(t); + } + + @Override + public void onComplete() { + try { + cleanup(channel); + } catch (Throwable t) { + subscriber.onError(new BufferEncodingException("Unexpected exception during encoding", t)); + return; + } + + subscriber.onComplete(); + } + }); + } + + private static EmbeddedChannel newEmbeddedChannel(final ChannelHandler handler, final BufferAllocator allocator) { + final EmbeddedChannel channel = new EmbeddedChannel(handler); + channel.config().setAllocator(getByteBufAllocator(allocator)); + return channel; + } + + @Nullable + private static Buffer drainChannelQueueToSingleBuffer(final Queue queue, final BufferAllocator allocator) { + if (queue.isEmpty()) { + return null; + } else if (queue.size() == 1) { + return newBufferFrom((ByteBuf) queue.poll()); + } else { + int accumulateSize = 0; + for (Object buffer : queue) { + accumulateSize += ((ByteBuf) buffer).readableBytes(); + } + + ByteBuf part; + if (accumulateSize <= MAX_SIZE_FOR_MERGED_BUFFER) { + // Try to merge everything together if total size is less than 1KiB (small chunks, ie. footer). + // CompositeBuffer can require some additional work to write and may limit how much data we can + // pass to writev (max of 1024 pointers), so if there are small chunks it may be better to combine them. + Buffer merged = allocator.newBuffer(); + while ((part = (ByteBuf) queue.poll()) != null) { + merged.writeBytes(newBufferFrom(part)); + } + + return merged; + } + + CompositeBuffer composite = allocator.newCompositeBuffer(Integer.MAX_VALUE); + while ((part = (ByteBuf) queue.poll()) != null) { + composite.addBuffer(newBufferFrom(part)); + } + + return composite; + } + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipCompressionBuilder.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipCompressionBuilder.java new file mode 100644 index 0000000000..e91b8ae1af --- /dev/null +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipCompressionBuilder.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2021 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.encoding.netty; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +/** + * Base class for Zip based content-codecs. + */ +public abstract class ZipCompressionBuilder { + private static final int DEFAULT_MAX_CHUNK_SIZE = 4 << 20; //4MiB + + private int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE; + private int compressionLevel = 6; + + ZipCompressionBuilder() { + // pkg private + } + + /** + * Sets the compression level for this codec's encoder. + * @param compressionLevel 1 yields the fastest compression and 9 yields the best compression, + * 0 means no compression. + * @return {@code this} + */ + public final ZipCompressionBuilder withCompressionLevel(final int compressionLevel) { + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException("compressionLevel: " + compressionLevel + " (expected: 0-9)"); + } + + this.compressionLevel = compressionLevel; + return this; + } + + /** + * Set the max allowed chunk size to inflate during decoding. + * @param maxChunkSize the max allowed chunk size to inflate during decoding. + * @return {@code this} + */ + public final ZipCompressionBuilder maxChunkSize(final int maxChunkSize) { + if (maxChunkSize <= 0) { + throw new IllegalArgumentException("maxChunkSize: " + maxChunkSize + " (expected > 0)"); + } + + this.maxChunkSize = maxChunkSize; + return this; + } + + /** + * Build and return an instance of the {@link SerializerDeserializer} with the configuration of the builder. + * @return the {@link SerializerDeserializer} with the configuration of the builder + */ + public abstract SerializerDeserializer build(); + + /** + * Build and return an instance of the {@link StreamingSerializerDeserializer} with the configuration of the + * builder. + * @return the {@link StreamingSerializerDeserializer} with the configuration of the builder + */ + public abstract StreamingSerializerDeserializer buildStreaming(); + + /** + * Returns the compression level for this codec. + * @return return the compression level set for this codec. + */ + final int compressionLevel() { + return compressionLevel; + } + + /** + * Returns the max chunk size allowed to inflate during decoding. + * @return Returns the max chunk size allowed to inflate during decoding. + */ + final int maxChunkSize() { + return maxChunkSize; + } +} diff --git a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipContentCodecBuilder.java b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipContentCodecBuilder.java index 3b172f64df..e7e5670244 100644 --- a/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipContentCodecBuilder.java +++ b/servicetalk-encoding-netty/src/main/java/io/servicetalk/encoding/netty/ZipContentCodecBuilder.java @@ -25,7 +25,9 @@ /** * Base class for Zip based content-codecs. + * @deprecated Use {@link ZipCompressionBuilder}. */ +@Deprecated public abstract class ZipContentCodecBuilder { private static final int DEFAULT_MAX_CHUNK_SIZE = 4 << 20; //4MiB diff --git a/servicetalk-encoding-netty/src/test/java/io/servicetalk/encoding/netty/NettyChannelContentCodecTest.java b/servicetalk-encoding-netty/src/test/java/io/servicetalk/encoding/netty/NettyChannelContentCodecTest.java index 7e56c73797..2075d098fd 100644 --- a/servicetalk-encoding-netty/src/test/java/io/servicetalk/encoding/netty/NettyChannelContentCodecTest.java +++ b/servicetalk-encoding-netty/src/test/java/io/servicetalk/encoding/netty/NettyChannelContentCodecTest.java @@ -46,8 +46,8 @@ import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; +@Deprecated class NettyChannelContentCodecTest { - private static final String INPUT; static { byte[] arr = new byte[1024]; diff --git a/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleClient.java b/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleClient.java index dbc2430c2b..7e9328daf7 100644 --- a/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleClient.java +++ b/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleClient.java @@ -15,68 +15,50 @@ */ package io.servicetalk.examples.grpc.compression; -import io.servicetalk.encoding.api.ContentCodec; -import io.servicetalk.encoding.api.Identity; -import io.servicetalk.encoding.netty.ContentCodings; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; +import io.servicetalk.grpc.api.DefaultGrpcClientMetadata; import io.servicetalk.grpc.netty.GrpcClients; -import io.grpc.examples.compression.Greeter; import io.grpc.examples.compression.Greeter.ClientFactory; import io.grpc.examples.compression.Greeter.GreeterClient; +import io.grpc.examples.compression.HelloReply; import io.grpc.examples.compression.HelloRequest; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.deflateDefault; /** * Extends the async "Hello World" example to include support for compression of the server response. */ public final class CompressionExampleClient { - - /** - * Encodings supported in preferred order. - */ - private static final List PREFERRED_ENCODINGS = - Collections.unmodifiableList(Arrays.asList( - // For the purposes of this example we disable GZip compression and use the - // server's second choice (deflate) to demonstrate that negotiation of compression algorithm is - // handled correctly. - // ContentCodings.gzipDefault(), - ContentCodings.deflateDefault(), - Identity.identity() - )); - - /** - * Metadata that, when provided to a sayHello, will cause the request to be compressed. - */ - private static final Greeter.SayHelloMetadata COMPRESS_REQUEST = new Greeter.SayHelloMetadata(ContentCodings.deflateDefault()); - public static void main(String... args) throws Exception { try (GreeterClient client = GrpcClients.forAddress("localhost", 8080).build(new ClientFactory() - // Requests will include 'Message-Accept-Encoding' HTTP header to inform the server which encodings we support. - .supportedMessageCodings(PREFERRED_ENCODINGS))) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(2); - + .bufferDecoderGroup(new BufferDecoderGroupBuilder() + // For the purposes of this example we disable GZip compression and use the + // server's second choice (deflate) to demonstrate that negotiation of compression algorithm is + // handled correctly. + // .add(NettyBufferEncoders.gzipDefault(), true) + .add(deflateDefault(), true) + .add(identityEncoder(), false).build()))) { // This request is sent with the request being uncompressed. The response may // be compressed because the ClientFactory will include the encodings we // support as a request header. - client.sayHello(HelloRequest.newBuilder().setName("Foo").build()) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(System.out::println); + Single respSingle1 = client.sayHello(HelloRequest.newBuilder().setName("NoMeta").build()) + .whenOnSuccess(System.out::println); // This request uses a different overload of the "sayHello" method that allows - // providing request metadata and we use it to request compression of the - // request. - client.sayHello(COMPRESS_REQUEST, HelloRequest.newBuilder().setName("Foo").build()) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(System.out::println); + // providing request metadata and we use it to request compression of the request. + Single respSingle2 = client.sayHello(new DefaultGrpcClientMetadata(deflateDefault()), + HelloRequest.newBuilder().setName("WithMeta").build()) + .whenOnSuccess(System.out::println); - responseProcessedLatch.await(); + // Issue the requests sequentially with concat. + respSingle1.concat(respSingle2) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleServer.java b/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleServer.java index 1b5d0339bc..d82d10fe6d 100644 --- a/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleServer.java +++ b/servicetalk-examples/grpc/compression/src/main/java/io/servicetalk/examples/grpc/compression/CompressionExampleServer.java @@ -15,65 +15,35 @@ */ package io.servicetalk.examples.grpc.compression; -import io.servicetalk.concurrent.api.Single; -import io.servicetalk.encoding.api.ContentCodec; -import io.servicetalk.encoding.api.Identity; -import io.servicetalk.encoding.netty.ContentCodings; -import io.servicetalk.grpc.api.GrpcServiceContext; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; import io.servicetalk.grpc.netty.GrpcServers; +import io.grpc.examples.compression.Greeter; import io.grpc.examples.compression.Greeter.GreeterService; -import io.grpc.examples.compression.Greeter.ServiceFactory; import io.grpc.examples.compression.HelloReply; -import io.grpc.examples.compression.HelloRequest; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.deflateDefault; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.gzipDefault; +import static java.util.Arrays.asList; /** * A simple extension of the gRPC "Hello World" example which demonstrates * compression of the request and response bodies. */ public class CompressionExampleServer { - - /** - * Supported encodings in preferred order. These will be matched against the list of encodings provided by the - * client to choose a mutually agreeable encoding. - */ - private static final List SUPPORTED_ENCODINGS = - Collections.unmodifiableList(Arrays.asList( - ContentCodings.gzipDefault(), - ContentCodings.deflateDefault(), - Identity.identity() - )); - public static void main(String... args) throws Exception { GrpcServers.forPort(8080) - // Create Greeter service which uses default binding to create ServiceFactory. - // (see {@link MyGreeterService#bindService}). Alternately a non-default binding could be used by - // directly creating a ServiceFactory directly. i.e. - // new ServiceFactory(new MyGreeterService(), strategyFactory, contentCodecs) - .listenAndAwait(new MyGreeterService()) + .listenAndAwait(new Greeter.ServiceFactory.Builder() + .bufferDecoderGroup(new BufferDecoderGroupBuilder() + .add(gzipDefault(), true) + .add(deflateDefault(), true) + .add(identityEncoder(), false).build()) + .bufferEncoders(asList(gzipDefault(), deflateDefault(), identityEncoder())) + .addService((GreeterService) (ctx, request) -> + succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build())) + .build()) .awaitShutdown(); } - - private static final class MyGreeterService implements GreeterService { - - @Override - public Single sayHello(final GrpcServiceContext ctx, final HelloRequest request) { - return succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); - } - - @Override - public ServiceFactory bindService() { - // Create a ServiceFactory bound to this service and includes the encodings supported for requests and - // the preferred encodings for responses. Responses will automatically be compressed if the request includes - // a mutually agreeable compression encoding that the client indicates they will accept and that the - // server supports. Requests using unsupported encodings receive an error response in the "grpc-status". - return new ServiceFactory(this, SUPPORTED_ENCODINGS); - } - } } diff --git a/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineClient.java b/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineClient.java index a299c85497..874234d23e 100644 --- a/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineClient.java +++ b/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineClient.java @@ -15,16 +15,18 @@ */ package io.servicetalk.examples.grpc.deadline; -import io.servicetalk.grpc.api.GrpcClientBuilder; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.grpc.api.DefaultGrpcClientMetadata; import io.servicetalk.grpc.netty.GrpcClients; -import io.servicetalk.transport.api.HostAndPort; -import io.grpc.examples.deadline.Greeter; +import io.grpc.examples.deadline.Greeter.ClientFactory; +import io.grpc.examples.deadline.Greeter.GreeterClient; +import io.grpc.examples.deadline.HelloReply; import io.grpc.examples.deadline.HelloRequest; -import java.net.InetSocketAddress; -import java.time.Duration; -import java.util.concurrent.CountDownLatch; +import static io.servicetalk.concurrent.api.Single.collectUnorderedDelayError; +import static java.time.Duration.ofMinutes; +import static java.time.Duration.ofSeconds; /** * Extends the async "Hello World!" example to demonstrate use of @@ -35,32 +37,28 @@ * @see gRPC and Deadlines */ public final class DeadlineClient { - public static void main(String... args) throws Exception { - GrpcClientBuilder builder = GrpcClients.forAddress("localhost", 8080) + try (GreeterClient client = GrpcClients.forAddress("localhost", 8080) // set the default timeout for completion of gRPC calls made using this client to 1 minute - .defaultTimeout(Duration.ofMinutes(1)); - try (Greeter.GreeterClient client = builder.build(new Greeter.ClientFactory())) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(2); - + .defaultTimeout(ofMinutes(1)).build(new ClientFactory())) { // Make a request using default timeout (this will succeed) - client.sayHello(HelloRequest.newBuilder().setName("Foo").build()) - .afterFinally(responseProcessedLatch::countDown) - .afterOnError(System.err::println) - .subscribe(System.out::println); + Single respSingle1 = + client.sayHello(HelloRequest.newBuilder().setName("DefaultTimeout").build()) + .whenOnError(System.err::println) + .whenOnSuccess(System.out::println); // Set the timeout for completion of this gRPC call to 3 seconds (this will timeout) - Greeter.SayHelloMetadata metadata = new Greeter.SayHelloMetadata(Duration.ofSeconds(3)); - client.sayHello(metadata, HelloRequest.newBuilder().setName("Bar").build()) - .afterFinally(responseProcessedLatch::countDown) - .afterOnError(System.err::println) - .subscribe(System.out::println); + Single respSingle2 = client.sayHello(new DefaultGrpcClientMetadata(ofSeconds(3)), + HelloRequest.newBuilder().setName("3SecondTimeout").build()) + .whenOnError(System.err::println) + .whenOnSuccess(System.out::println); - // block until responses are complete and both afterFinally() have been called - responseProcessedLatch.await(); + // Issue the requests in parallel. + collectUnorderedDelayError(respSingle1, respSingle2) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineServer.java b/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineServer.java index 39e9cf62a9..8e1c75f1a2 100644 --- a/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineServer.java +++ b/servicetalk-examples/grpc/deadline/src/main/java/io/servicetalk/examples/grpc/deadline/DeadlineServer.java @@ -15,18 +15,14 @@ */ package io.servicetalk.examples.grpc.deadline; -import io.servicetalk.concurrent.api.Single; -import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.netty.GrpcServers; -import io.grpc.examples.deadline.Greeter; +import io.grpc.examples.deadline.Greeter.GreeterService; import io.grpc.examples.deadline.HelloReply; -import io.grpc.examples.deadline.HelloRequest; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; import static io.servicetalk.concurrent.api.Single.succeeded; +import static java.time.Duration.ofMinutes; +import static java.time.Duration.ofSeconds; /** * Extends the async "Hello World!" example to demonstrate use of @@ -37,31 +33,14 @@ * @see gRPC and Deadlines */ public class DeadlineServer { - public static void main(String... args) throws Exception { GrpcServers.forPort(8080) // Set default timeout for completion of RPC calls made to this server - .defaultTimeout(Duration.ofMinutes(2)) - .listenAndAwait(new MyGreeterService()) + .defaultTimeout(ofMinutes(2)) + .listenAndAwait((GreeterService) (ctx, request) -> + // Force a 5 second delay in the response. + ctx.executionContext().executor().timer(ofSeconds(5)).concat( + succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()))) .awaitShutdown(); } - - private static final class MyGreeterService implements Greeter.GreeterService { - - @Override - public Single sayHello(final GrpcServiceContext ctx, final HelloRequest request) { - - // Force a 5 second delay in the response. - return Single.defer(() -> { - try { - TimeUnit.SECONDS.sleep(5); - } catch (InterruptedException woken) { - Thread.interrupted(); - } - - return succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()) - .subscribeShareContext(); - }); - } - } } diff --git a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldClient.java b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldClient.java index aa4ace1ff5..f6d03705ca 100644 --- a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldClient.java +++ b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldClient.java @@ -21,8 +21,6 @@ import io.grpc.examples.helloworld.Greeter.GreeterClient; import io.grpc.examples.helloworld.HelloRequest; -import java.util.concurrent.CountDownLatch; - /** * Implementation of the * gRPC hello world example @@ -31,19 +29,14 @@ * Start the {@link HelloWorldServer} first. */ public final class HelloWorldClient { - public static void main(String... args) throws Exception { try (GreeterClient client = GrpcClients.forAddress("localhost", 8080).build(new ClientFactory())) { + client.sayHello(HelloRequest.newBuilder().setName("World").build()) + .whenOnSuccess(System.out::println) // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - client.sayHello(HelloRequest.newBuilder().setName("Foo").build()) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(System.out::println); - - // block until response is complete and afterFinally() is called - responseProcessedLatch.await(); + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldServer.java b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldServer.java index e743030477..037e5d0ae3 100644 --- a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldServer.java +++ b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/async/HelloWorldServer.java @@ -15,13 +15,10 @@ */ package io.servicetalk.examples.grpc.helloworld.async; -import io.servicetalk.concurrent.api.Single; -import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.netty.GrpcServers; import io.grpc.examples.helloworld.Greeter.GreeterService; import io.grpc.examples.helloworld.HelloReply; -import io.grpc.examples.helloworld.HelloRequest; import static io.servicetalk.concurrent.api.Single.succeeded; @@ -33,18 +30,10 @@ * Start this server first and then run the {@link HelloWorldClient}. */ public class HelloWorldServer { - public static void main(String... args) throws Exception { GrpcServers.forPort(8080) - .listenAndAwait(new MyGreeterService()) + .listenAndAwait((GreeterService) (ctx, request) -> + succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build())) .awaitShutdown(); } - - private static final class MyGreeterService implements GreeterService { - - @Override - public Single sayHello(final GrpcServiceContext ctx, final HelloRequest request) { - return succeeded(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()); - } - } } diff --git a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldClient.java b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldClient.java index a9c296d9c3..799cc137cf 100644 --- a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldClient.java +++ b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldClient.java @@ -23,11 +23,10 @@ import io.grpc.examples.helloworld.HelloRequest; public final class BlockingHelloWorldClient { - public static void main(String[] args) throws Exception { try (BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080) .buildBlocking(new ClientFactory())) { - HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("Foo").build()); + HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("World").build()); System.out.println(reply); } } diff --git a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldServer.java b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldServer.java index 2294928811..0137cf8b2f 100644 --- a/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldServer.java +++ b/servicetalk-examples/grpc/helloworld/src/main/java/io/servicetalk/examples/grpc/helloworld/blocking/BlockingHelloWorldServer.java @@ -15,27 +15,16 @@ */ package io.servicetalk.examples.grpc.helloworld.blocking; -import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.netty.GrpcServers; import io.grpc.examples.helloworld.Greeter.BlockingGreeterService; -import io.grpc.examples.helloworld.Greeter.ServiceFactory; import io.grpc.examples.helloworld.HelloReply; -import io.grpc.examples.helloworld.HelloRequest; public class BlockingHelloWorldServer { - public static void main(String[] args) throws Exception { GrpcServers.forPort(8080) - .listenAndAwait(new ServiceFactory(new MyGreeterService())) + .listenAndAwait((BlockingGreeterService) (ctx, request) -> + HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()) .awaitShutdown(); } - - private static final class MyGreeterService implements BlockingGreeterService { - - @Override - public HelloReply sayHello(final GrpcServiceContext ctx, final HelloRequest request) { - return HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); - } - } } diff --git a/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsClient.java b/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsClient.java index 3ef7212d97..0a13d2f67e 100644 --- a/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsClient.java +++ b/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsClient.java @@ -17,16 +17,16 @@ import io.servicetalk.grpc.netty.GrpcClients; -import io.grpc.examples.helloworld.GreeterSt; +import io.grpc.examples.helloworld.GreeterSt.BlockingGreeterClient; +import io.grpc.examples.helloworld.GreeterSt.ClientFactory; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest; public final class BlockingProtocOptionsClient { - public static void main(String[] args) throws Exception { - try (GreeterSt.BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080) - .buildBlocking(new GreeterSt.ClientFactory())) { - HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("Foo").build()); + try (BlockingGreeterClient client = GrpcClients.forAddress("localhost", 8080) + .buildBlocking(new ClientFactory())) { + HelloReply reply = client.sayHello(HelloRequest.newBuilder().setName("Options").build()); System.out.println(reply); } } diff --git a/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java b/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java index eb9827c8ab..3a8711fe73 100644 --- a/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java +++ b/servicetalk-examples/grpc/protoc-options/src/main/java/io/servicetalk/examples/grpc/protocoptions/BlockingProtocOptionsServer.java @@ -15,26 +15,16 @@ */ package io.servicetalk.examples.grpc.protocoptions; -import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.netty.GrpcServers; -import io.grpc.examples.helloworld.GreeterSt; +import io.grpc.examples.helloworld.GreeterSt.BlockingGreeterService; import io.grpc.examples.helloworld.HelloReply; -import io.grpc.examples.helloworld.HelloRequest; public class BlockingProtocOptionsServer { - public static void main(String[] args) throws Exception { GrpcServers.forPort(8080) - .listenAndAwait(new GreeterSt.ServiceFactory(new MyGreeterService())) + .listenAndAwait((BlockingGreeterService) (ctx, request) -> + HelloReply.newBuilder().setMessage("Hello " + request.getName()).build()) .awaitShutdown(); } - - private static final class MyGreeterService implements GreeterSt.BlockingGreeterService { - - @Override - public HelloReply sayHello(final GrpcServiceContext ctx, final HelloRequest request) { - return HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); - } - } } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideClient.java index 91066212f8..242864472a 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideClient.java @@ -21,22 +21,16 @@ import io.grpc.examples.routeguide.RouteGuide; import io.grpc.examples.routeguide.RouteGuide.ClientFactory; -import java.util.concurrent.CountDownLatch; - public final class RouteGuideClient { - public static void main(String[] args) throws Exception { try (RouteGuide.RouteGuideClient client = GrpcClients.forAddress("localhost", 8080) .build(new ClientFactory())) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.getFeature(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(System.out::println); - - responseProcessedLatch.await(); + .whenOnSuccess(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideServer.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideServer.java index b1b4fd39e5..e2a4cde013 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideServer.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/RouteGuideServer.java @@ -41,7 +41,6 @@ import io.grpc.examples.routeguide.Point; import io.grpc.examples.routeguide.Rectangle; import io.grpc.examples.routeguide.RouteGuide.RouteGuideService; -import io.grpc.examples.routeguide.RouteGuide.ServiceFactory; import io.grpc.examples.routeguide.RouteNote; import io.grpc.examples.routeguide.RouteSummary; @@ -59,14 +58,13 @@ import static java.lang.Math.min; import static java.util.function.Function.identity; -public class RouteGuideServer { - +public final class RouteGuideServer { public static void main(String[] args) throws Exception { FeaturesFinder featuresFinder = args.length > 0 ? fromJson(RouteGuideServer.class.getResource(args[0])) : randomFeatures(); GrpcServers.forPort(8080) - .listenAndAwait(new ServiceFactory(new DefaultRouteGuideService(featuresFinder))) + .listenAndAwait(new DefaultRouteGuideService(featuresFinder)) .awaitShutdown(); } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideRequestStreamingClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideRequestStreamingClient.java index 259a119b2f..ab459c160d 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideRequestStreamingClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideRequestStreamingClient.java @@ -18,28 +18,21 @@ import io.servicetalk.grpc.netty.GrpcClients; import io.grpc.examples.routeguide.Point; -import io.grpc.examples.routeguide.RouteGuide; import io.grpc.examples.routeguide.RouteGuide.ClientFactory; - -import java.util.concurrent.CountDownLatch; +import io.grpc.examples.routeguide.RouteGuide.RouteGuideClient; import static io.servicetalk.concurrent.api.Publisher.from; public final class RouteGuideRequestStreamingClient { - public static void main(String[] args) throws Exception { - try (RouteGuide.RouteGuideClient client = GrpcClients.forAddress("localhost", 8080) - .build(new ClientFactory())) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); + try (RouteGuideClient client = GrpcClients.forAddress("localhost", 8080).build(new ClientFactory())) { client.recordRoute(from(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build(), Point.newBuilder().setLatitude(789000).setLongitude(-789000).build())) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(System.out::println); - - responseProcessedLatch.await(); + .whenOnSuccess(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideResponseStreamingClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideResponseStreamingClient.java index 1bacafa9f7..a0fdb1dfb0 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideResponseStreamingClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideResponseStreamingClient.java @@ -19,28 +19,20 @@ import io.grpc.examples.routeguide.Point; import io.grpc.examples.routeguide.Rectangle; -import io.grpc.examples.routeguide.RouteGuide; import io.grpc.examples.routeguide.RouteGuide.ClientFactory; - -import java.util.concurrent.CountDownLatch; +import io.grpc.examples.routeguide.RouteGuide.RouteGuideClient; public final class RouteGuideResponseStreamingClient { - public static void main(String[] args) throws Exception { - try (RouteGuide.RouteGuideClient client = GrpcClients.forAddress("localhost", 8080) - .build(new ClientFactory())) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); + try (RouteGuideClient client = GrpcClients.forAddress("localhost", 8080).build(new ClientFactory())) { client.listFeatures(Rectangle.newBuilder() .setHi(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .setLo(Point.newBuilder().setLatitude(789000).setLongitude(-789000).build()) - .build()) - .afterFinally(responseProcessedLatch::countDown) - .forEach(System.out::println); - - responseProcessedLatch.await(); + .setLo(Point.newBuilder().setLatitude(789000).setLongitude(-789000).build()).build()) + .whenOnNext(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideStreamingClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideStreamingClient.java index 96d5e0bbb0..e11e8069d6 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideStreamingClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/async/streaming/RouteGuideStreamingClient.java @@ -18,35 +18,26 @@ import io.servicetalk.grpc.netty.GrpcClients; import io.grpc.examples.routeguide.Point; -import io.grpc.examples.routeguide.RouteGuide; import io.grpc.examples.routeguide.RouteGuide.ClientFactory; +import io.grpc.examples.routeguide.RouteGuide.RouteGuideClient; import io.grpc.examples.routeguide.RouteNote; -import java.util.concurrent.CountDownLatch; - import static io.servicetalk.concurrent.api.Publisher.from; public final class RouteGuideStreamingClient { - public static void main(String[] args) throws Exception { - try (RouteGuide.RouteGuideClient client = GrpcClients.forAddress("localhost", 8080) - .build(new ClientFactory())) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); + try (RouteGuideClient client = GrpcClients.forAddress("localhost", 8080).build(new ClientFactory())) { client.routeChat(from(RouteNote.newBuilder() - .setLocation(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .setMessage("First note.") - .build(), + .setLocation(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) + .setMessage("First note.").build(), RouteNote.newBuilder() - .setLocation(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .setMessage("Querying notes.") - .build())) - .afterFinally(responseProcessedLatch::countDown) - .forEach(System.out::println); - - responseProcessedLatch.await(); + .setLocation(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) + .setMessage("Querying notes.").build())) + .whenOnNext(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/BlockingRouteGuideClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/BlockingRouteGuideClient.java index 54ca0b8840..2b0ca0bcd3 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/BlockingRouteGuideClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/BlockingRouteGuideClient.java @@ -23,7 +23,6 @@ import io.grpc.examples.routeguide.RouteGuide.ClientFactory; public final class BlockingRouteGuideClient { - public static void main(String[] args) throws Exception { try (RouteGuide.BlockingRouteGuideClient client = GrpcClients.forAddress("localhost", 8080) .buildBlocking(new ClientFactory())) { diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideRequestStreamingClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideRequestStreamingClient.java index 2db58fc267..62bf01969f 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideRequestStreamingClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideRequestStreamingClient.java @@ -25,7 +25,6 @@ import static java.util.Arrays.asList; public final class BlockingRouteGuideRequestStreamingClient { - public static void main(String[] args) throws Exception { try (BlockingRouteGuideClient client = GrpcClients.forAddress("localhost", 8080) .buildBlocking(new ClientFactory())) { diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideResponseStreamingClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideResponseStreamingClient.java index f186f714ed..9ba456d846 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideResponseStreamingClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideResponseStreamingClient.java @@ -25,15 +25,13 @@ import io.grpc.examples.routeguide.RouteGuide.ClientFactory; public final class BlockingRouteGuideResponseStreamingClient { - public static void main(String[] args) throws Exception { try (BlockingRouteGuideClient client = GrpcClients.forAddress("localhost", 8080) .buildBlocking(new ClientFactory())) { BlockingIterable features = client.listFeatures( Rectangle.newBuilder() .setHi(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .setLo(Point.newBuilder().setLatitude(789000).setLongitude(-789000).build()) - .build()); + .setLo(Point.newBuilder().setLatitude(789000).setLongitude(-789000).build()).build()); for (Feature feature : features) { System.out.println(feature); } diff --git a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideStreamingClient.java b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideStreamingClient.java index 8b3d38b479..ab4354670e 100644 --- a/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideStreamingClient.java +++ b/servicetalk-examples/grpc/routeguide/src/main/java/io/servicetalk/examples/grpc/routeguide/blocking/streaming/BlockingRouteGuideStreamingClient.java @@ -27,7 +27,6 @@ import static io.servicetalk.concurrent.api.Processors.newBlockingIterableProcessor; public final class BlockingRouteGuideStreamingClient { - public static void main(String[] args) throws Exception { try (BlockingRouteGuideClient client = GrpcClients.forAddress("localhost", 8080) .buildBlocking(new ClientFactory())) { @@ -36,12 +35,10 @@ public static void main(String[] args) throws Exception { response = client.routeChat(request).iterator(); request.next(RouteNote.newBuilder() .setLocation(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .setMessage("First note.") - .build()); + .setMessage("First note.").build()); request.next(RouteNote.newBuilder() .setLocation(Point.newBuilder().setLatitude(123456).setLongitude(-123456).build()) - .setMessage("Querying notes.") - .build()); + .setMessage("Querying notes.").build()); } while (response.hasNext()) { System.out.println(response.next()); diff --git a/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleClient.java b/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleClient.java index 1d6500c95a..91585e4a5f 100644 --- a/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleClient.java +++ b/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleClient.java @@ -15,74 +15,63 @@ */ package io.servicetalk.examples.http.compression; -import io.servicetalk.encoding.api.ContentCodec; -import io.servicetalk.encoding.api.Identity; -import io.servicetalk.encoding.netty.ContentCodings; -import io.servicetalk.http.api.ContentCodingHttpRequesterFilter; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; +import io.servicetalk.http.api.ContentEncodingHttpRequesterFilter; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpRequest; +import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.netty.HttpClients; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.deflateDefault; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.gzipDefault; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** - * Extends the async "Hello World" example to include compression of the request - * and response bodies. + * Extends the async "Hello World" example to include compression of the request and response bodies. */ public final class CompressionFilterExampleClient { - - /** - * Encodings in preferred order. - */ - private static final List PREFERRED_ENCODINGS = - Collections.unmodifiableList(Arrays.asList( - ContentCodings.gzipDefault(), - ContentCodings.deflateDefault(), - Identity.identity() - )); - public static void main(String... args) throws Exception { try (HttpClient client = HttpClients.forSingleAddress("localhost", 8080) // Adds filter that provides compression for the request body when a request sets the encoding. // Also sets the accept encoding header for the server's response. - .appendClientFilter(new ContentCodingHttpRequesterFilter(PREFERRED_ENCODINGS)) + .appendClientFilter(new ContentEncodingHttpRequesterFilter(new BufferDecoderGroupBuilder() + // For the purposes of this example we disable GZip compression and use the + // server's second choice (deflate) to demonstrate that negotiation of compression algorithm is + // handled correctly. + // .add(NettyBufferEncoders.gzipDefault(), true) + .add(deflateDefault(), true) + .add(identityEncoder(), false).build())) .build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(2); - // Make a request with an uncompressed payload. - HttpRequest request = client.post("/sayHello") - // Request will be sent with no compression, which has the same effect as setting encoding to identity - // .encoding(ContentCodings.identity()) - .payloadBody("George", textSerializer()); - client.request(request) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + HttpRequest request = client.post("/sayHello1") + // Request will be sent with no compression, same effect as setting encoding to identity + .contentEncoding(identityEncoder()) + .payloadBody("World1", textSerializerUtf8()); + Single respSingle1 = client.request(request) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); + System.out.println(resp.payloadBody(textSerializerUtf8())); }); // Make a request with an gzip compressed payload. - request = client.post("/sayHello") + request = client.post("/sayHello2") // Encode the request using gzip. - .encoding(ContentCodings.gzipDefault()) - .payloadBody("Gracie", textSerializer()); - client.request(request) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .contentEncoding(gzipDefault()) + .payloadBody("World2", textSerializerUtf8()); + Single respSingle2 = client.request(request) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); + System.out.println(resp.payloadBody(textSerializerUtf8())); }); - responseProcessedLatch.await(); + // Issue the requests sequentially with concat. + respSingle1.concat(respSingle2) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleServer.java b/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleServer.java index 622ee95e5b..fd51c5495c 100644 --- a/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleServer.java +++ b/servicetalk-examples/http/compression/src/main/java/io/servicetalk/examples/http/compression/CompressionFilterExampleServer.java @@ -15,60 +15,32 @@ */ package io.servicetalk.examples.http.compression; -import io.servicetalk.encoding.api.ContentCodec; -import io.servicetalk.encoding.api.Identity; -import io.servicetalk.encoding.netty.ContentCodings; -import io.servicetalk.http.api.ContentCodingHttpServiceFilter; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; +import io.servicetalk.http.api.ContentEncodingHttpServiceFilter; import io.servicetalk.http.netty.HttpServers; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.deflateDefault; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.gzipDefault; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; +import static java.util.Arrays.asList; /** - * Extends the async "Hello World" sample to add support for compression of - * request and response payload bodies. + * Extends the async "Hello World" sample to add support for compression of request and response payload bodies. */ public final class CompressionFilterExampleServer { - - /** - * Supported encodings for the request. Requests using unsupported encodings will receive an HTTP 415 - * "Unsupported Media Type" response. - */ - private static final List SUPPORTED_REQ_ENCODINGS = - Collections.unmodifiableList(Arrays.asList( - ContentCodings.gzipDefault(), - ContentCodings.deflateDefault(), - Identity.identity() - )); - - /** - * Supported encodings for the response in preferred order. These will be matched against the list of encodings - * provided by the client to choose a mutually agreeable encoding. - */ - private static final List SUPPORTED_RESP_ENCODINGS = - Collections.unmodifiableList(Arrays.asList( - // For the purposes of this example we disable GZip for the response compression and use the client's second - // choice (deflate) to demonstrate that negotiation of compression algorithm is handled correctly. - /* ContentCodings.gzipDefault(), */ - ContentCodings.deflateDefault(), - Identity.identity() - )); - public static void main(String... args) throws Exception { HttpServers.forPort(8080) - // Adds a content coding service filter that includes the encodings supported for requests and the - // preferred encodings for responses. Responses will automatically be compressed if the request includes - // a mutually agreeable compression encoding that the client indicates they will accept and that the - // server supports. - .appendServiceFilter(new ContentCodingHttpServiceFilter(SUPPORTED_REQ_ENCODINGS, SUPPORTED_RESP_ENCODINGS)) + .appendServiceFilter(new ContentEncodingHttpServiceFilter( + asList(gzipDefault(), deflateDefault(), identityEncoder()), + new BufferDecoderGroupBuilder() + .add(gzipDefault(), true) + .add(deflateDefault(), true) + .add(identityEncoder(), false).build())) .listenAndAwait((ctx, request, responseFactory) -> { - String who = request.payloadBody(textDeserializer()); - return succeeded(responseFactory.ok().payloadBody("Hello " + who + "!", textSerializer())); + String who = request.payloadBody(textSerializerUtf8()); + return succeeded(responseFactory.ok().payloadBody("Hello " + who + "!", textSerializerUtf8())); }) .awaitShutdown(); } diff --git a/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleClient.java b/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleClient.java index 9253f01add..8cfacc22ca 100644 --- a/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleClient.java +++ b/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleClient.java @@ -18,18 +18,15 @@ import io.servicetalk.concurrent.api.AsyncContext; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpExecutionStrategy; -import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.SingleAddressHttpClientBuilder; import io.servicetalk.http.netty.H2ProtocolConfigBuilder; import io.servicetalk.http.netty.HttpClients; import io.servicetalk.http.netty.HttpProtocolConfigs; import io.servicetalk.logging.api.LogLevel; -import java.util.concurrent.CountDownLatch; import java.util.function.BooleanSupplier; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.logging.api.LogLevel.TRACE; /** @@ -166,23 +163,15 @@ public static void main(String... args) throws Exception { .enableFrameLogging("servicetalk-examples-h2-frame-logger", TRACE, Boolean.TRUE::booleanValue) .build()) .build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - - // Make a request with a payload. - HttpRequest request = client.post("/sayHello") - .payloadBody("George", textSerializer()); - client.request(request) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + client.request(client.post("/sayHello").payloadBody("George", textSerializerUtf8())) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); - }); - - // block until request is complete and afterFinally() is called - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(textSerializerUtf8())); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleServer.java b/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleServer.java index f27eb39ac4..44ddc99af6 100644 --- a/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleServer.java +++ b/servicetalk-examples/http/debugging/src/main/java/io/servicetalk/examples/http/debugging/DebuggingExampleServer.java @@ -26,8 +26,7 @@ import java.util.function.BooleanSupplier; import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.logging.api.LogLevel.TRACE; /** @@ -159,8 +158,8 @@ public static void main(String... args) throws Exception { .enableFrameLogging("servicetalk-examples-h2-frame-logger", TRACE, Boolean.TRUE::booleanValue) .build()) .listenAndAwait((ctx, request, responseFactory) -> { - String who = request.payloadBody(textDeserializer()); - return succeeded(responseFactory.ok().payloadBody("Hello " + who + "!", textSerializer())); + String who = request.payloadBody(textSerializerUtf8()); + return succeeded(responseFactory.ok().payloadBody("Hello " + who + "!", textSerializerUtf8())); }) .awaitShutdown(); } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldClient.java index 035c8c002c..cd7b65007a 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldClient.java @@ -18,27 +18,20 @@ import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; public final class HelloWorldClient { - public static void main(String[] args) throws Exception { try (HttpClient client = HttpClients.forSingleAddress("localhost", 8080).build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.request(client.get("/sayHello")) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); - }); - - // block until request is complete and afterFinally() is called - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(textSerializerUtf8())); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldServer.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldServer.java index d6a9dc258b..523da00358 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldServer.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldServer.java @@ -18,15 +18,14 @@ import io.servicetalk.http.netty.HttpServers; import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; public final class HelloWorldServer { - public static void main(String[] args) throws Exception { HttpServers.forPort(8080) .listenAndAwait((ctx, request, responseFactory) -> succeeded(responseFactory.ok() - .payloadBody("Hello World!", textSerializer()))) + .payloadBody("Hello World!", textSerializerUtf8()))) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldUrlClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldUrlClient.java index 5101346569..fb14faf20b 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldUrlClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/HelloWorldUrlClient.java @@ -18,26 +18,20 @@ import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; public final class HelloWorldUrlClient { - public static void main(String[] args) throws Exception { try (HttpClient client = HttpClients.forMultiAddressUrl().build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.request(client.get("http://localhost:8080/sayHello")) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); - }); - - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(textSerializerUtf8())); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingClient.java index 31a5d6da8b..af37c2ba97 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingClient.java @@ -18,25 +18,19 @@ import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; public final class HelloWorldStreamingClient { - public static void main(String[] args) throws Exception { try (StreamingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildStreaming()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.request(client.get("/sayHello")) .beforeOnSuccess(response -> System.out.println(response.toString((name, value) -> value))) - .flatMapPublisher(resp -> resp.payloadBody(textDeserializer())) - .afterFinally(responseProcessedLatch::countDown) - .forEach(System.out::println); - - responseProcessedLatch.await(); + .flatMapPublisher(resp -> resp.payloadBody(appSerializerUtf8FixLen())) + .whenOnNext(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingServer.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingServer.java index f23d9deb09..206201c75e 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingServer.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingServer.java @@ -19,15 +19,14 @@ import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; public final class HelloWorldStreamingServer { - public static void main(String[] args) throws Exception { HttpServers.forPort(8080) .listenStreamingAndAwait((ctx, request, responseFactory) -> succeeded(responseFactory.ok() - .payloadBody(from("Hello", " World!"), textSerializer()))) + .payloadBody(from("Hello", " World!"), appSerializerUtf8FixLen()))) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingUrlClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingUrlClient.java index b9c0975c6e..07b28d699c 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingUrlClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/async/streaming/HelloWorldStreamingUrlClient.java @@ -18,25 +18,19 @@ import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; public final class HelloWorldStreamingUrlClient { - public static void main(String[] args) throws Exception { try (StreamingHttpClient client = HttpClients.forMultiAddressUrl().buildStreaming()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.request(client.get("http://localhost:8080/sayHello")) .beforeOnSuccess(response -> System.out.println(response.toString((name, value) -> value))) - .flatMapPublisher(resp -> resp.payloadBody(textDeserializer())) - .afterFinally(responseProcessedLatch::countDown) - .forEach(System.out::println); - - responseProcessedLatch.await(); + .flatMapPublisher(resp -> resp.payloadBody(appSerializerUtf8FixLen())) + .whenOnNext(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .ignoreElements().toFuture().get(); } } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldClient.java index f7b609c614..1913a24a31 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldClient.java @@ -19,7 +19,7 @@ import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; public final class BlockingHelloWorldClient { @@ -27,7 +27,7 @@ public static void main(String[] args) throws Exception { try (BlockingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildBlocking()) { HttpResponse response = client.request(client.get("/sayHello")); System.out.println(response.toString((name, value) -> value)); - System.out.println(response.payloadBody(textDeserializer())); + System.out.println(response.payloadBody(textSerializerUtf8())); } } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldServer.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldServer.java index a22d880154..e105fe02d5 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldServer.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldServer.java @@ -17,14 +17,14 @@ import io.servicetalk.http.netty.HttpServers; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; public final class BlockingHelloWorldServer { public static void main(String[] args) throws Exception { HttpServers.forPort(8080) .listenBlockingAndAwait((ctx, request, responseFactory) -> - responseFactory.ok().payloadBody("Hello World!", textSerializer())) + responseFactory.ok().payloadBody("Hello World!", textSerializerUtf8())) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldUrlClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldUrlClient.java index 675e3fc2db..2af643f1a1 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldUrlClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/BlockingHelloWorldUrlClient.java @@ -19,7 +19,7 @@ import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; public final class BlockingHelloWorldUrlClient { @@ -27,7 +27,7 @@ public static void main(String[] args) throws Exception { try (BlockingHttpClient client = HttpClients.forMultiAddressUrl().buildBlocking()) { HttpResponse response = client.request(client.get("http://localhost:8080/sayHello")); System.out.println(response.toString((name, value) -> value)); - System.out.println(response.payloadBody(textDeserializer())); + System.out.println(response.payloadBody(textSerializerUtf8())); } } } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingClient.java index 403d123423..92d1ddd267 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingClient.java @@ -20,7 +20,7 @@ import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; public final class BlockingHelloWorldStreamingClient { @@ -29,7 +29,7 @@ public static void main(String[] args) throws Exception { .buildBlockingStreaming()) { BlockingStreamingHttpResponse response = client.request(client.get("/sayHello")); System.out.println(response.toString((name, value) -> value)); - try (BlockingIterator payload = response.payloadBody(textDeserializer()).iterator()) { + try (BlockingIterator payload = response.payloadBody(appSerializerUtf8FixLen()).iterator()) { while (payload.hasNext()) { System.out.println(payload.next()); } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingServer.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingServer.java index a1e33469b8..24f2a7214c 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingServer.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingServer.java @@ -18,13 +18,13 @@ import io.servicetalk.http.api.HttpPayloadWriter; import io.servicetalk.http.netty.HttpServers; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; public final class BlockingHelloWorldStreamingServer { public static void main(String[] args) throws Exception { HttpServers.forPort(8080).listenBlockingStreamingAndAwait((ctx, request, response) -> { - try (HttpPayloadWriter payloadWriter = response.sendMetaData(textSerializer())) { + try (HttpPayloadWriter payloadWriter = response.sendMetaData(appSerializerUtf8FixLen())) { payloadWriter.write("Hello"); payloadWriter.write(" World!"); } diff --git a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingUrlClient.java b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingUrlClient.java index 621eadb9ac..0d38f1f162 100644 --- a/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingUrlClient.java +++ b/servicetalk-examples/http/helloworld/src/main/java/io/servicetalk/examples/http/helloworld/blocking/streaming/BlockingHelloWorldStreamingUrlClient.java @@ -20,7 +20,7 @@ import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; public final class BlockingHelloWorldStreamingUrlClient { @@ -28,7 +28,7 @@ public static void main(String[] args) throws Exception { try (BlockingStreamingHttpClient client = HttpClients.forMultiAddressUrl().buildBlockingStreaming()) { BlockingStreamingHttpResponse response = client.request(client.get("http://localhost:8080/sayHello")); System.out.println(response.toString((name, value) -> value)); - try (BlockingIterator payload = response.payloadBody(textDeserializer()).iterator()) { + try (BlockingIterator payload = response.payloadBody(appSerializerUtf8FixLen()).iterator()) { while (payload.hasNext()) { System.out.println(payload.next()); } diff --git a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpClientWithAlpn.java b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpClientWithAlpn.java index 75778ec987..26668c22ab 100644 --- a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpClientWithAlpn.java +++ b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpClientWithAlpn.java @@ -21,7 +21,7 @@ import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; @@ -41,7 +41,7 @@ public static void main(String[] args) throws Exception { .buildBlocking()) { HttpResponse response = client.request(client.get("/")); System.out.println(response.toString((name, value) -> value)); - System.out.println(response.payloadBody(textDeserializer())); + System.out.println(response.payloadBody(textSerializerUtf8())); } } } diff --git a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpServerWithAlpn.java b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpServerWithAlpn.java index ee5f928f88..0421a41b27 100644 --- a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpServerWithAlpn.java +++ b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/alpn/HttpServerWithAlpn.java @@ -19,7 +19,7 @@ import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ServerSslConfigBuilder; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; @@ -37,8 +37,8 @@ public static void main(String[] args) throws Exception { .build()) // Note: this example demonstrates only blocking-aggregated programming paradigm, for asynchronous and // streaming API see helloworld examples. - .listenBlockingAndAwait((ctx, request, responseFactory) -> - responseFactory.ok().payloadBody("I negotiate HTTP/2 or HTTP/1.1 via ALPN!", textSerializer())) + .listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok() + .payloadBody("I negotiate HTTP/2 or HTTP/1.1 via ALPN!", textSerializerUtf8())) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeClient.java b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeClient.java index 2f24b99349..831ae560b0 100644 --- a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeClient.java +++ b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeClient.java @@ -19,7 +19,7 @@ import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; /** @@ -35,7 +35,7 @@ public static void main(String[] args) throws Exception { .buildBlocking()) { HttpResponse response = client.request(client.get("/")); System.out.println(response.toString((name, value) -> value)); - System.out.println(response.payloadBody(textDeserializer())); + System.out.println(response.payloadBody(textSerializerUtf8())); } } } diff --git a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeServer.java b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeServer.java index 8d7bb944f9..14a525cca9 100644 --- a/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeServer.java +++ b/servicetalk-examples/http/http2/src/main/java/io/servicetalk/examples/http/http2/priorknowledge/Http2PriorKnowledgeServer.java @@ -17,7 +17,7 @@ import io.servicetalk.http.netty.HttpServers; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; /** @@ -31,7 +31,7 @@ public static void main(String[] args) throws Exception { // Note: this example demonstrates only blocking-aggregated programming paradigm, for asynchronous and // streaming API see helloworld examples. .listenBlockingAndAwait((ctx, request, responseFactory) -> - responseFactory.ok().payloadBody("I speak HTTP/2!", textSerializer())) + responseFactory.ok().payloadBody("I speak HTTP/2!", textSerializerUtf8())) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoClient.java b/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoClient.java index 4a6050ba38..188ebfc13c 100644 --- a/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoClient.java +++ b/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoClient.java @@ -22,7 +22,7 @@ import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LANGUAGE; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * Demonstrates a few features: @@ -62,7 +62,7 @@ public static void main(String[] args) throws Exception { response.headers().get(CONTENT_LANGUAGE)); } - String responseBody = response.payloadBody(textDeserializer()); + String responseBody = response.payloadBody(textSerializerUtf8()); System.out.println("Response body: " + responseBody); } } diff --git a/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoServer.java b/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoServer.java index 7fda60fc19..000c515a51 100644 --- a/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoServer.java +++ b/servicetalk-examples/http/metadata/src/main/java/io/servicetalk/examples/http/metadata/MetaDataDemoServer.java @@ -18,7 +18,7 @@ import io.servicetalk.http.netty.HttpServers; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LANGUAGE; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * Demonstration server to be used with {@link MetaDataDemoClient}. @@ -55,7 +55,7 @@ public static void main(String[] args) throws Exception { // Return the language in upper case, to demonstrate the case-insensitive compare // in the client. .addHeader(CONTENT_LANGUAGE, languageCode.toUpperCase()) - .payloadBody(helloText, textSerializer()); + .payloadBody(helloText, textSerializerUtf8()); }) .awaitShutdown(); } diff --git a/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpClientMutualTLS.java b/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpClientMutualTLS.java index ef02cf6083..2012d136ab 100644 --- a/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpClientMutualTLS.java +++ b/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpClientMutualTLS.java @@ -21,7 +21,7 @@ import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * A client that does mutual TLS. @@ -38,7 +38,7 @@ public static void main(String[] args) throws Exception { .buildBlocking()) { HttpResponse response = client.request(client.get("/")); System.out.println(response.toString((name, value) -> value)); - System.out.println(response.payloadBody(textDeserializer())); + System.out.println(response.payloadBody(textSerializerUtf8())); } } } diff --git a/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpServerMutualTLS.java b/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpServerMutualTLS.java index a26fbdebd4..4e587cf2dc 100644 --- a/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpServerMutualTLS.java +++ b/servicetalk-examples/http/mutual-tls/src/main/java/io/servicetalk/examples/http/mutualtls/HttpServerMutualTLS.java @@ -20,7 +20,7 @@ import io.servicetalk.transport.api.ServerSslConfigBuilder; import io.servicetalk.transport.api.SslClientAuthMode; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * A server that does mutual TLS. @@ -36,8 +36,8 @@ public static void main(String[] args) throws Exception { .trustManager(DefaultTestCerts::loadClientCAPem).build()) // Note: this example demonstrates only blocking-aggregated programming paradigm, for asynchronous and // streaming API see helloworld examples. - .listenBlockingAndAwait((ctx, request, responseFactory) -> - responseFactory.ok().payloadBody("Client and Server completed Mutual TLS!", textSerializer())) + .listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok() + .payloadBody("Client and Server completed Mutual TLS!", textSerializerUtf8())) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/ManualRedirectClient.java b/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/ManualRedirectClient.java index efbfa7871b..ae647a8c34 100644 --- a/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/ManualRedirectClient.java +++ b/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/ManualRedirectClient.java @@ -21,15 +21,13 @@ import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; -import java.util.concurrent.CountDownLatch; - import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.examples.http.redirects.RedirectingServer.CUSTOM_HEADER; import static io.servicetalk.examples.http.redirects.RedirectingServer.NON_SECURE_SERVER_PORT; import static io.servicetalk.examples.http.redirects.RedirectingServer.SECURE_SERVER_PORT; import static io.servicetalk.http.api.HttpHeaderNames.LOCATION; import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.REDIRECTION_3XX; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * Async "Hello World" example that demonstrates how redirects can be handled manually when single-address clients are @@ -41,7 +39,6 @@ * */ public final class ManualRedirectClient { - public static void main(String... args) throws Exception { try (HttpClient secureClient = HttpClients.forSingleAddress("localhost", SECURE_SERVER_PORT) // The custom SSL configuration here is necessary only because this example uses self-signed @@ -50,11 +47,6 @@ public static void main(String... args) throws Exception { .sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem).build()).build()) { try (HttpClient client = HttpClients.forSingleAddress("localhost", NON_SECURE_SERVER_PORT).build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - // Redirect of a GET request with a custom header: HttpRequest originalGet = client.get("http://localhost:8080/sayHello") .addHeader(CUSTOM_HEADER, "value"); @@ -70,17 +62,17 @@ public static void main(String... args) throws Exception { // Decided not to follow redirect, return the original response or an error: return succeeded(response); }) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); + System.out.println(resp.payloadBody(textSerializerUtf8())); System.out.println(); - }); - - responseProcessedLatch.await(); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); // Redirect of a POST request with a payload body: - responseProcessedLatch = new CountDownLatch(1); HttpRequest originalPost = client.post("http://localhost:8080/sayHello") .payloadBody(client.executionContext().bufferAllocator().fromAscii("some_content")); client.request(originalPost) @@ -95,13 +87,14 @@ public static void main(String... args) throws Exception { // Decided not to follow redirect, return the original response or an error: return succeeded(response); }) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); - }); - - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(textSerializerUtf8())); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/RedirectWithStateUrlClient.java b/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/RedirectWithStateUrlClient.java index c5cebcdcac..1576a78801 100644 --- a/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/RedirectWithStateUrlClient.java +++ b/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/RedirectWithStateUrlClient.java @@ -29,8 +29,6 @@ import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; -import java.util.concurrent.CountDownLatch; - import static io.servicetalk.concurrent.api.AsyncContextMap.Key.newKey; import static io.servicetalk.examples.http.redirects.RedirectingServer.CUSTOM_HEADER; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LENGTH; @@ -38,7 +36,7 @@ import static io.servicetalk.http.api.HttpHeaderValues.CHUNKED; import static io.servicetalk.http.api.HttpRequestMethod.POST; import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.REDIRECTION_3XX; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * Async `Hello World` example that demonstrates how redirects can be handled automatically by a @@ -114,20 +112,17 @@ protected Single request(StreamingHttpRequester delegate, } }) .build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.request(client.post("http://localhost:8080/sayHello") .addHeader(CUSTOM_HEADER, "value") .payloadBody(client.executionContext().bufferAllocator().fromAscii("some_content"))) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); - }); - - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(textSerializerUtf8())); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/SimpleRedirectUrlClient.java b/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/SimpleRedirectUrlClient.java index 69982a1ad3..7116db5019 100644 --- a/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/SimpleRedirectUrlClient.java +++ b/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/SimpleRedirectUrlClient.java @@ -20,9 +20,7 @@ import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * Async `Hello World` example that demonstrates how redirects can be handled automatically by a @@ -31,7 +29,6 @@ * Automatic redirects have limitations. See {@link RedirectWithStateUrlClient} for more information. */ public final class SimpleRedirectUrlClient { - public static void main(String... args) throws Exception { try (HttpClient client = HttpClients.forMultiAddressUrl() // This is an optional configuration that applies more restrictive limit for the number or redirects: @@ -45,18 +42,15 @@ public static void main(String... args) throws Exception { } }) .build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); client.request(client.get("http://localhost:8080/sayHello")) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); - }); - - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(textSerializerUtf8())); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/SerializerUtils.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/SerializerUtils.java new file mode 100644 index 0000000000..066e23d0d5 --- /dev/null +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/SerializerUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2021 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.examples.http.serialization; + +import io.servicetalk.http.api.HttpSerializerDeserializer; +import io.servicetalk.http.api.HttpStreamingSerializerDeserializer; + +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; +import static io.servicetalk.http.api.HttpSerializers.jsonSerializer; +import static io.servicetalk.http.api.HttpSerializers.jsonStreamingSerializer; + +/** + * Utilities to cache POJO to JSON serializer instances. + */ +public final class SerializerUtils { + public static final HttpSerializerDeserializer REQ_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(CreatePojoRequest.class)); + + public static final HttpStreamingSerializerDeserializer REQ_STREAMING_SERIALIZER = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(CreatePojoRequest.class)); + + public static final HttpSerializerDeserializer RESP_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(PojoResponse.class)); + + public static final HttpStreamingSerializerDeserializer RESP_STREAMING_SERIALIZER = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(PojoResponse.class)); + + private SerializerUtils() { + } +} diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoClient.java index 27997e0bcc..1de6621ff9 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoClient.java @@ -15,36 +15,26 @@ */ package io.servicetalk.examples.http.serialization.async; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; -import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_SERIALIZER; public final class PojoClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (HttpClient client = HttpClients.forSingleAddress("localhost", 8080).build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - client.request(client.post("/pojos") - .payloadBody(new CreatePojoRequest("value"), serializer.serializerFor(CreatePojoRequest.class))) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .payloadBody(new CreatePojoRequest("value"), REQ_SERIALIZER)) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(serializer.deserializerFor(PojoResponse.class))); - }); - - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(RESP_SERIALIZER)); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoServer.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoServer.java index 12ebe830d7..1b7cf73b7b 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoServer.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoServer.java @@ -15,23 +15,20 @@ */ package io.servicetalk.examples.http.serialization.async; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; import io.servicetalk.examples.http.serialization.PojoResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpServers; import java.util.concurrent.ThreadLocalRandom; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_SERIALIZER; import static io.servicetalk.http.api.HttpHeaderNames.ALLOW; import static io.servicetalk.http.api.HttpRequestMethod.POST; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; public final class PojoServer { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); HttpServers.forPort(8080) .listenAndAwait((ctx, request, responseFactory) -> { if (!"/pojos".equals(request.requestTarget())) { @@ -40,10 +37,10 @@ public static void main(String[] args) throws Exception { if (!POST.equals(request.method())) { return succeeded(responseFactory.methodNotAllowed().addHeader(ALLOW, POST.name())); } - CreatePojoRequest req = request.payloadBody(serializer.deserializerFor(CreatePojoRequest.class)); + CreatePojoRequest req = request.payloadBody(REQ_SERIALIZER); return succeeded(responseFactory.created() .payloadBody(new PojoResponse(ThreadLocalRandom.current().nextInt(100), req.getValue()), - serializer.serializerFor(PojoResponse.class))); + RESP_SERIALIZER)); }) .awaitShutdown(); } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoUrlClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoUrlClient.java index 0a99c13670..3aa93a5756 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoUrlClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/PojoUrlClient.java @@ -15,36 +15,26 @@ */ package io.servicetalk.examples.http.serialization.async; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; -import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_SERIALIZER; public final class PojoUrlClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (HttpClient client = HttpClients.forMultiAddressUrl().build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - client.request(client.post("http://localhost:8080/pojos") - .payloadBody(new CreatePojoRequest("value"), serializer.serializerFor(CreatePojoRequest.class))) - .afterFinally(responseProcessedLatch::countDown) - .subscribe(resp -> { + .payloadBody(new CreatePojoRequest("value"), REQ_SERIALIZER)) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(serializer.deserializerFor(PojoResponse.class))); - }); - - responseProcessedLatch.await(); + System.out.println(resp.payloadBody(RESP_SERIALIZER)); + }) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingClient.java index 3634c0f474..09fc69e4c2 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingClient.java @@ -15,37 +15,27 @@ */ package io.servicetalk.examples.http.serialization.async.streaming; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; -import io.servicetalk.examples.http.serialization.PojoResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - import static io.servicetalk.concurrent.api.Publisher.from; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_STREAMING_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_STREAMING_SERIALIZER; public final class PojoStreamingClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (StreamingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildStreaming()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - client.request(client.post("/pojos") .payloadBody(from("value1", "value2", "value3").map(CreatePojoRequest::new), - serializer.serializerFor(CreatePojoRequest.class))) + REQ_STREAMING_SERIALIZER)) .beforeOnSuccess(response -> System.out.println(response.toString((name, value) -> value))) - .flatMapPublisher(resp -> resp.payloadBody(serializer.deserializerFor(PojoResponse.class))) - .afterFinally(responseProcessedLatch::countDown) - .forEach(System.out::println); - - responseProcessedLatch.await(); + .flatMapPublisher(resp -> resp.payloadBody(RESP_STREAMING_SERIALIZER)) + .whenOnNext(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingServer.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingServer.java index b4e9e7d1bc..3c995651d2 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingServer.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingServer.java @@ -15,24 +15,20 @@ */ package io.servicetalk.examples.http.serialization.async.streaming; -import io.servicetalk.data.jackson.JacksonSerializationProvider; -import io.servicetalk.examples.http.serialization.CreatePojoRequest; import io.servicetalk.examples.http.serialization.PojoResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpServers; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_STREAMING_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_STREAMING_SERIALIZER; import static io.servicetalk.http.api.HttpHeaderNames.ALLOW; import static io.servicetalk.http.api.HttpRequestMethod.POST; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; public final class PojoStreamingServer { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); HttpServers.forPort(8080) .listenStreamingAndAwait((ctx, request, responseFactory) -> { if (!"/pojos".equals(request.requestTarget())) { @@ -43,9 +39,9 @@ public static void main(String[] args) throws Exception { } AtomicInteger newId = new AtomicInteger(ThreadLocalRandom.current().nextInt(100)); return succeeded(responseFactory.created() - .payloadBody(request.payloadBody(serializer.deserializerFor(CreatePojoRequest.class)) + .payloadBody(request.payloadBody(REQ_STREAMING_SERIALIZER) .map(req -> new PojoResponse(newId.getAndIncrement(), req.getValue())), - serializer.serializerFor(PojoResponse.class))); + RESP_STREAMING_SERIALIZER)); }) .awaitShutdown(); } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingUrlClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingUrlClient.java index 3dba999425..1e89520d99 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingUrlClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/async/streaming/PojoStreamingUrlClient.java @@ -15,37 +15,27 @@ */ package io.servicetalk.examples.http.serialization.async.streaming; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; -import io.servicetalk.examples.http.serialization.PojoResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.netty.HttpClients; -import java.util.concurrent.CountDownLatch; - import static io.servicetalk.concurrent.api.Publisher.from; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_STREAMING_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_STREAMING_SERIALIZER; public final class PojoStreamingUrlClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (StreamingHttpClient client = HttpClients.forMultiAddressUrl().buildStreaming()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(1); - client.request(client.post("http://localhost:8080/pojos") .payloadBody(from("value1", "value2", "value3").map(CreatePojoRequest::new), - serializer.serializerFor(CreatePojoRequest.class))) + REQ_STREAMING_SERIALIZER)) .beforeOnSuccess(response -> System.out.println(response.toString((name, value) -> value))) - .flatMapPublisher(resp -> resp.payloadBody(serializer.deserializerFor(PojoResponse.class))) - .afterFinally(responseProcessedLatch::countDown) - .forEach(System.out::println); - - responseProcessedLatch.await(); + .flatMapPublisher(resp -> resp.payloadBody(RESP_STREAMING_SERIALIZER)) + .whenOnNext(System.out::println) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoClient.java index 8ce36887a8..3ca3f3dfdc 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoClient.java @@ -15,25 +15,21 @@ */ package io.servicetalk.examples.http.serialization.blocking; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; -import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.HttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_SERIALIZER; public final class BlockingPojoClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (BlockingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildBlocking()) { HttpResponse resp = client.request(client.post("/pojos") - .payloadBody(new CreatePojoRequest("value"), serializer.serializerFor(CreatePojoRequest.class))); + .payloadBody(new CreatePojoRequest("value"), REQ_SERIALIZER)); System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(serializer.deserializerFor(PojoResponse.class))); + System.out.println(resp.payloadBody(RESP_SERIALIZER)); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoServer.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoServer.java index b41ea6162c..c200592c0b 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoServer.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoServer.java @@ -15,22 +15,19 @@ */ package io.servicetalk.examples.http.serialization.blocking; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; import io.servicetalk.examples.http.serialization.PojoResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpServers; import java.util.concurrent.ThreadLocalRandom; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_SERIALIZER; import static io.servicetalk.http.api.HttpHeaderNames.ALLOW; import static io.servicetalk.http.api.HttpRequestMethod.POST; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; public final class BlockingPojoServer { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); HttpServers.forPort(8080) .listenBlockingAndAwait((ctx, request, responseFactory) -> { if (!"/pojos".equals(request.requestTarget())) { @@ -39,10 +36,10 @@ public static void main(String[] args) throws Exception { if (!POST.equals(request.method())) { return responseFactory.methodNotAllowed().addHeader(ALLOW, POST.name()); } - CreatePojoRequest req = request.payloadBody(serializer.deserializerFor(CreatePojoRequest.class)); + CreatePojoRequest req = request.payloadBody(REQ_SERIALIZER); return responseFactory.created() .payloadBody(new PojoResponse(ThreadLocalRandom.current().nextInt(100), req.getValue()), - serializer.serializerFor(PojoResponse.class)); + RESP_SERIALIZER); }) .awaitShutdown(); } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoUrlClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoUrlClient.java index 8f0f40ca5b..3b970daa6d 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoUrlClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/BlockingPojoUrlClient.java @@ -15,25 +15,21 @@ */ package io.servicetalk.examples.http.serialization.blocking; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; -import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.HttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_SERIALIZER; public final class BlockingPojoUrlClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (BlockingHttpClient client = HttpClients.forMultiAddressUrl().buildBlocking()) { HttpResponse resp = client.request(client.post("http://localhost:8080/pojos") - .payloadBody(new CreatePojoRequest("value"), serializer.serializerFor(CreatePojoRequest.class))); + .payloadBody(new CreatePojoRequest("value"), REQ_SERIALIZER)); System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(serializer.deserializerFor(PojoResponse.class))); + System.out.println(resp.payloadBody(RESP_SERIALIZER)); } } } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingClient.java index c44d1f1cd5..e1b60986d8 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingClient.java @@ -16,30 +16,28 @@ package io.servicetalk.examples.http.serialization.blocking.streaming; import io.servicetalk.concurrent.BlockingIterator; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.BlockingStreamingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_STREAMING_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_STREAMING_SERIALIZER; import static java.util.Arrays.asList; public final class BlockingPojoStreamingClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (BlockingStreamingHttpClient client = HttpClients.forSingleAddress("localhost", 8080).buildBlockingStreaming()) { BlockingStreamingHttpResponse response = client.request(client.post("/pojos") .payloadBody(asList( - new CreatePojoRequest("value1"), new CreatePojoRequest("value2"), new CreatePojoRequest("value3")), - serializer.serializerFor(CreatePojoRequest.class))); + new CreatePojoRequest("value1"), + new CreatePojoRequest("value2"), + new CreatePojoRequest("value3")), + REQ_STREAMING_SERIALIZER)); System.out.println(response); - try (BlockingIterator payload = - response.payloadBody(serializer.deserializerFor(PojoResponse.class)).iterator()) { + try (BlockingIterator payload = response.payloadBody(RESP_STREAMING_SERIALIZER).iterator()) { while (payload.hasNext()) { System.out.println(payload.next()); } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingServer.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingServer.java index 1446c884c3..e0bb32aad2 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingServer.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingServer.java @@ -16,28 +16,25 @@ package io.servicetalk.examples.http.serialization.blocking.streaming; import io.servicetalk.concurrent.BlockingIterable; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.HttpPayloadWriter; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpServers; import java.util.concurrent.atomic.AtomicInteger; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_STREAMING_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_STREAMING_SERIALIZER; import static io.servicetalk.http.api.HttpHeaderNames.ALLOW; import static io.servicetalk.http.api.HttpRequestMethod.POST; import static io.servicetalk.http.api.HttpResponseStatus.CREATED; import static io.servicetalk.http.api.HttpResponseStatus.METHOD_NOT_ALLOWED; import static io.servicetalk.http.api.HttpResponseStatus.NOT_FOUND; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; public final class BlockingPojoStreamingServer { - private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); HttpServers.forPort(8080) .listenBlockingStreamingAndAwait((ctx, request, response) -> { if (!"/pojos".equals(request.requestTarget())) { @@ -50,13 +47,11 @@ public static void main(String[] args) throws Exception { .sendMetaData() .close(); } else { - BlockingIterable values = request - .payloadBody(serializer.deserializerFor(CreatePojoRequest.class)); + BlockingIterable values = request.payloadBody(REQ_STREAMING_SERIALIZER); response.status(CREATED); - try (HttpPayloadWriter writer = response.sendMetaData( - serializer.serializerFor(PojoResponse.class))) { - + try (HttpPayloadWriter writer = + response.sendMetaData(RESP_STREAMING_SERIALIZER)) { for (CreatePojoRequest req : values) { writer.write(new PojoResponse(ID_GENERATOR.getAndIncrement(), req.getValue())); } diff --git a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingUrlClient.java b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingUrlClient.java index c85e397200..a6ff2241e9 100644 --- a/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingUrlClient.java +++ b/servicetalk-examples/http/serialization/src/main/java/io/servicetalk/examples/http/serialization/blocking/streaming/BlockingPojoStreamingUrlClient.java @@ -16,29 +16,27 @@ package io.servicetalk.examples.http.serialization.blocking.streaming; import io.servicetalk.concurrent.BlockingIterator; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.examples.http.serialization.CreatePojoRequest; import io.servicetalk.examples.http.serialization.PojoResponse; import io.servicetalk.http.api.BlockingStreamingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.netty.HttpClients; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.examples.http.serialization.SerializerUtils.REQ_STREAMING_SERIALIZER; +import static io.servicetalk.examples.http.serialization.SerializerUtils.RESP_STREAMING_SERIALIZER; import static java.util.Arrays.asList; public final class BlockingPojoStreamingUrlClient { - public static void main(String[] args) throws Exception { - HttpSerializationProvider serializer = jsonSerializer(new JacksonSerializationProvider()); try (BlockingStreamingHttpClient client = HttpClients.forMultiAddressUrl().buildBlockingStreaming()) { BlockingStreamingHttpResponse response = client.request(client.post("http://localhost:8080/pojos") .payloadBody(asList( - new CreatePojoRequest("value1"), new CreatePojoRequest("value2"), new CreatePojoRequest("value3")), - serializer.serializerFor(CreatePojoRequest.class))); + new CreatePojoRequest("value1"), + new CreatePojoRequest("value2"), + new CreatePojoRequest("value3")), + REQ_STREAMING_SERIALIZER)); System.out.println(response); - try (BlockingIterator payload = - response.payloadBody(serializer.deserializerFor(PojoResponse.class)).iterator()) { + try (BlockingIterator payload = response.payloadBody(RESP_STREAMING_SERIALIZER).iterator()) { while (payload.hasNext()) { System.out.println(payload.next()); } diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BadResponseHandlingServiceFilter.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BadResponseHandlingServiceFilter.java index 5d4e0f8596..e82f48dc9e 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BadResponseHandlingServiceFilter.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BadResponseHandlingServiceFilter.java @@ -25,10 +25,8 @@ import io.servicetalk.http.api.StreamingHttpServiceFilter; import io.servicetalk.http.api.StreamingHttpServiceFilterFactory; -import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.failed; -import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * Example service filter that returns a response with the exception message if the wrapped service completes with a @@ -45,8 +43,8 @@ public Single handle(HttpServiceContext ctx, StreamingHtt if (cause instanceof BadResponseStatusException) { // It's useful to include the exception message in the payload for demonstration purposes, but // this is not recommended in production as it may leak internal information. - return succeeded(responseFactory.internalServerError() - .payloadBody(from(cause.getMessage()), textSerializer())); + return responseFactory.internalServerError().toResponse().map( + resp -> resp.payloadBody(cause.getMessage(), textSerializerUtf8()).toStreamingResponse()); } return failed(cause); }); diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BlockingGatewayService.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BlockingGatewayService.java index 2bb950af10..544e175bc2 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BlockingGatewayService.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/BlockingGatewayService.java @@ -25,9 +25,7 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpServiceContext; -import io.servicetalk.serialization.api.TypeHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +33,13 @@ import java.util.ArrayList; import java.util.List; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.ENTITY_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.FULL_RECOMMEND_LIST_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.METADATA_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RATING_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RECOMMEND_LIST_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_SERIALIZER; import static io.servicetalk.examples.http.service.composition.backends.ErrorResponseGeneratingServiceFilter.SIMULATE_ERROR_QP_NAME; /** @@ -42,19 +47,8 @@ * JSON array containing all {@link FullRecommendation}s. */ final class BlockingGatewayService implements BlockingHttpService { - private static final Logger LOGGER = LoggerFactory.getLogger(BlockingGatewayService.class); - private static final TypeHolder> typeOfRecommendation = - new TypeHolder>() { }; - private static final TypeHolder> typeOfFullRecommendations = - new TypeHolder>() { }; - - private static final String USER_ID_QP_NAME = "userId"; - private static final String ENTITY_ID_QP_NAME = "entityId"; - - private final HttpSerializationProvider serializers; - private final BlockingHttpClient recommendationClient; private final BlockingHttpClient metadataClient; private final BlockingHttpClient ratingClient; @@ -63,13 +57,11 @@ final class BlockingGatewayService implements BlockingHttpService { BlockingGatewayService(final BlockingHttpClient recommendationClient, final BlockingHttpClient metadataClient, final BlockingHttpClient ratingClient, - final BlockingHttpClient userClient, - final HttpSerializationProvider serializers) { + final BlockingHttpClient userClient) { this.recommendationClient = recommendationClient; this.metadataClient = metadataClient; this.ratingClient = ratingClient; this.userClient = userClient; - this.serializers = serializers; } @Override @@ -85,7 +77,7 @@ public HttpResponse handle(final HttpServiceContext ctx, final HttpRequest reque recommendationClient.request(recommendationClient.get("/recommendations/aggregated") .addQueryParameter(USER_ID_QP_NAME, userId) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) - .payloadBody(serializers.deserializerFor(typeOfRecommendation)); + .payloadBody(RECOMMEND_LIST_SERIALIZER); List fullRecommendations = new ArrayList<>(recommendations.size()); for (Recommendation recommendation : recommendations) { @@ -94,20 +86,20 @@ public HttpResponse handle(final HttpServiceContext ctx, final HttpRequest reque metadataClient.request(metadataClient.get("/metadata") .addQueryParameter(ENTITY_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) - .payloadBody(serializers.deserializerFor(Metadata.class)); + .payloadBody(METADATA_SERIALIZER); final User user = userClient.request(userClient.get("/user") .addQueryParameter(USER_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) - .payloadBody(serializers.deserializerFor(User.class)); + .payloadBody(USER_SERIALIZER); Rating rating; try { rating = ratingClient.request(ratingClient.get("/rating") .addQueryParameter(ENTITY_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) - .payloadBody(serializers.deserializerFor(Rating.class)); + .payloadBody(RATING_SERIALIZER); } catch (Exception cause) { // We consider ratings to be a non-critical data and hence we substitute the response // with a static "unavailable" rating when the rating service is unavailable or provides @@ -119,7 +111,6 @@ public HttpResponse handle(final HttpServiceContext ctx, final HttpRequest reque fullRecommendations.add(new FullRecommendation(metadata, user, rating)); } - return responseFactory.ok() - .payloadBody(fullRecommendations, serializers.serializerFor(typeOfFullRecommendations)); + return responseFactory.ok().payloadBody(fullRecommendations, FULL_RECOMMEND_LIST_SERIALIZER); } } diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayServer.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayServer.java index d2ed0c094a..53cc750c5e 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayServer.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayServer.java @@ -16,9 +16,7 @@ package io.servicetalk.examples.http.service.composition; import io.servicetalk.concurrent.api.CompositeCloseable; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.api.StreamingHttpService; import io.servicetalk.http.netty.HttpClients; @@ -38,7 +36,6 @@ import static io.servicetalk.examples.http.service.composition.backends.PortRegistry.RATINGS_BACKEND_ADDRESS; import static io.servicetalk.examples.http.service.composition.backends.PortRegistry.RECOMMENDATIONS_BACKEND_ADDRESS; import static io.servicetalk.examples.http.service.composition.backends.PortRegistry.USER_BACKEND_ADDRESS; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; import static io.servicetalk.transport.netty.NettyIoExecutors.createIoExecutor; import static java.time.Duration.ofMillis; import static java.time.Duration.ofSeconds; @@ -75,26 +72,21 @@ public static void main(String[] args) throws Exception { HttpClient ratingsClient = newClient(ioExecutor, RATINGS_BACKEND_ADDRESS, resources, "ratings backend").asClient(); - // Use Jackson for serialization and deserialization. - // HttpSerializer validates HTTP metadata for serialization/deserialization and also provides higher level - // HTTP focused serialization APIs. - HttpSerializationProvider httpSerializer = jsonSerializer(new JacksonSerializationProvider()); - // Gateway supports different endpoints for blocking, streaming or aggregated implementations. // We create a router to express these endpoints. HttpPredicateRouterBuilder routerBuilder = new HttpPredicateRouterBuilder(); final StreamingHttpService gatewayService = routerBuilder.whenPathStartsWith("/recommendations/stream") .thenRouteTo(new StreamingGatewayService(recommendationsClient, metadataClient, - ratingsClient, userClient, httpSerializer)) + ratingsClient, userClient)) .whenPathStartsWith("/recommendations/aggregated") .thenRouteTo(new GatewayService(recommendationsClient.asClient(), - metadataClient, ratingsClient, userClient, httpSerializer)) + metadataClient, ratingsClient, userClient)) .whenPathStartsWith("/recommendations/blocking") .thenRouteTo(new BlockingGatewayService(recommendationsClient.asBlockingClient(), metadataClient.asBlockingClient(), ratingsClient.asBlockingClient(), - userClient.asBlockingClient(), httpSerializer)) + userClient.asBlockingClient())) .buildStreaming(); // Create configurable starter for HTTP server. diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayService.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayService.java index 086c27d200..6cb9265f9b 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayService.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/GatewayService.java @@ -25,10 +25,8 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpService; import io.servicetalk.http.api.HttpServiceContext; -import io.servicetalk.serialization.api.TypeHolder; import java.util.ArrayList; import java.util.List; @@ -36,6 +34,13 @@ import static io.servicetalk.concurrent.api.Publisher.fromIterable; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.api.Single.zip; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.ENTITY_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.FULL_RECOMMEND_LIST_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.METADATA_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RATING_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RECOMMEND_LIST_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_SERIALIZER; import static io.servicetalk.examples.http.service.composition.backends.ErrorResponseGeneratingServiceFilter.SIMULATE_ERROR_QP_NAME; /** @@ -43,29 +48,17 @@ * response. */ final class GatewayService implements HttpService { - - private static final TypeHolder> typeOfRecommendation = - new TypeHolder>() { }; - private static final TypeHolder> typeOfFullRecommendation = - new TypeHolder>() { }; - - private static final String USER_ID_QP_NAME = "userId"; - private static final String ENTITY_ID_QP_NAME = "entityId"; - private final HttpClient recommendationsClient; private final HttpClient metadataClient; private final HttpClient ratingsClient; private final HttpClient userClient; - private final HttpSerializationProvider serializers; GatewayService(final HttpClient recommendationsClient, final HttpClient metadataClient, - final HttpClient ratingsClient, final HttpClient userClient, - HttpSerializationProvider serializers) { + final HttpClient ratingsClient, final HttpClient userClient) { this.recommendationsClient = recommendationsClient; this.metadataClient = metadataClient; this.ratingsClient = ratingsClient; this.userClient = userClient; - this.serializers = serializers; } @Override @@ -82,10 +75,10 @@ public Single handle(final HttpServiceContext ctx, .addQueryParameter(USER_ID_QP_NAME, userId) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) // Since HTTP payload is a buffer, we deserialize into List>. - .map(response -> response.payloadBody(serializers.deserializerFor(typeOfRecommendation))) + .map(response -> response.payloadBody(RECOMMEND_LIST_SERIALIZER)) .flatMap(recommendations -> queryRecommendationDetails(recommendations, errorQpValues)) .map(fullRecommendations -> responseFactory.ok() - .payloadBody(fullRecommendations, serializers.serializerFor(typeOfFullRecommendation))); + .payloadBody(fullRecommendations, FULL_RECOMMEND_LIST_SERIALIZER)); } private Single> queryRecommendationDetails(List recommendations, @@ -99,21 +92,21 @@ private Single> queryRecommendationDetails(List response.payloadBody(serializers.deserializerFor(Metadata.class))); + .map(response -> response.payloadBody(METADATA_SERIALIZER)); Single user = userClient.request(userClient.get("/user") .addQueryParameter(USER_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) // Since HTTP payload is a buffer, we deserialize into User. - .map(response -> response.payloadBody(serializers.deserializerFor(User.class))); + .map(response -> response.payloadBody(USER_SERIALIZER)); Single rating = ratingsClient.request(ratingsClient.get("/rating") .addQueryParameter(ENTITY_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) // Since HTTP payload is a buffer, we deserialize into Rating. - .map(response -> response.payloadBody(serializers.deserializerFor(Rating.class))) + .map(response -> response.payloadBody(RATING_SERIALIZER)) // We consider ratings to be a non-critical data and hence we substitute the // response with a static "unavailable" rating when the rating service is // unavailable or provides a bad response. This is typically referred to as a diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/SerializerUtils.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/SerializerUtils.java new file mode 100644 index 0000000000..0ec95d7adb --- /dev/null +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/SerializerUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2021 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.examples.http.service.composition; + +import io.servicetalk.examples.http.service.composition.pojo.FullRecommendation; +import io.servicetalk.examples.http.service.composition.pojo.Metadata; +import io.servicetalk.examples.http.service.composition.pojo.Rating; +import io.servicetalk.examples.http.service.composition.pojo.Recommendation; +import io.servicetalk.examples.http.service.composition.pojo.User; +import io.servicetalk.http.api.HttpSerializerDeserializer; +import io.servicetalk.http.api.HttpStreamingSerializerDeserializer; + +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.List; + +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; +import static io.servicetalk.http.api.HttpSerializers.jsonSerializer; +import static io.servicetalk.http.api.HttpSerializers.jsonStreamingSerializer; + +public final class SerializerUtils { + private static final TypeReference> RECOMMEND_LIST_TYPE = + new TypeReference>() { }; + private static final TypeReference> FULL_RECOMMEND_LIST_TYPE = + new TypeReference>() { }; + public static final HttpSerializerDeserializer USER_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(User.class)); + public static final HttpSerializerDeserializer RATING_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(Rating.class)); + public static final HttpSerializerDeserializer METADATA_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(Metadata.class)); + public static final HttpSerializerDeserializer> RECOMMEND_LIST_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(RECOMMEND_LIST_TYPE)); + public static final HttpSerializerDeserializer> FULL_RECOMMEND_LIST_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(FULL_RECOMMEND_LIST_TYPE)); + public static final HttpStreamingSerializerDeserializer RECOMMEND_STREAM_SERIALIZER = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(Recommendation.class)); + public static final HttpStreamingSerializerDeserializer FULL_RECOMMEND_STREAM_SERIALIZER = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(FullRecommendation.class)); + + public static final String USER_ID_QP_NAME = "userId"; + public static final String ENTITY_ID_QP_NAME = "entityId"; + + private SerializerUtils() { + } +} diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/StreamingGatewayService.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/StreamingGatewayService.java index a1d55e6575..02b77f711d 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/StreamingGatewayService.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/StreamingGatewayService.java @@ -36,6 +36,13 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.api.Single.zip; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.ENTITY_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.FULL_RECOMMEND_STREAM_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.METADATA_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RATING_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RECOMMEND_STREAM_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_SERIALIZER; import static io.servicetalk.examples.http.service.composition.backends.ErrorResponseGeneratingServiceFilter.SIMULATE_ERROR_QP_NAME; /** @@ -43,26 +50,19 @@ * {@link FullRecommendation} objects as JSON. */ final class StreamingGatewayService implements StreamingHttpService { - private static final Logger LOGGER = LoggerFactory.getLogger(StreamingGatewayService.class); - private static final String USER_ID_QP_NAME = "userId"; - private static final String ENTITY_ID_QP_NAME = "entityId"; - private final StreamingHttpClient recommendationsClient; private final HttpClient metadataClient; private final HttpClient ratingsClient; private final HttpClient userClient; - private final HttpSerializationProvider serializers; StreamingGatewayService(final StreamingHttpClient recommendationsClient, final HttpClient metadataClient, - final HttpClient ratingsClient, final HttpClient userClient, - HttpSerializationProvider serializers) { + final HttpClient ratingsClient, final HttpClient userClient) { this.recommendationsClient = recommendationsClient; this.metadataClient = metadataClient; this.ratingsClient = ratingsClient; this.userClient = userClient; - this.serializers = serializers; } @Override @@ -79,8 +79,7 @@ public Single handle(final HttpServiceContext ctx, final .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) .map(response -> response.transformPayloadBody(recommendations -> queryRecommendationDetails(recommendations, errorQpValues), - serializers.deserializerFor(Recommendation.class), - serializers.serializerFor(FullRecommendation.class))); + RECOMMEND_STREAM_SERIALIZER, FULL_RECOMMEND_STREAM_SERIALIZER)); } private Publisher queryRecommendationDetails(Publisher recommendations, @@ -91,21 +90,21 @@ private Publisher queryRecommendationDetails(Publisher response.payloadBody(serializers.deserializerFor(Metadata.class))); + .map(response -> response.payloadBody(METADATA_SERIALIZER)); Single user = userClient.request(userClient.get("/user") .addQueryParameter(USER_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) // Since HTTP payload is a buffer, we deserialize into User. - .map(response -> response.payloadBody(serializers.deserializerFor(User.class))); + .map(response -> response.payloadBody(USER_SERIALIZER)); Single rating = ratingsClient.request(ratingsClient.get("/rating") .addQueryParameter(ENTITY_ID_QP_NAME, recommendation.getEntityId()) .addQueryParameters(SIMULATE_ERROR_QP_NAME, errorQpValues)) // Since HTTP payload is a buffer, we deserialize into Rating. - .map(response -> response.payloadBody(serializers.deserializerFor(Rating.class))) + .map(response -> response.payloadBody(RATING_SERIALIZER)) // We consider ratings to be a non-critical data and hence we substitute the response // with a static "unavailable" rating when the rating service is unavailable or provides // a bad response. This is typically referred to as a "fallback". diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/BackendsStarter.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/BackendsStarter.java index 56cdb09c85..8f3cd455d5 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/BackendsStarter.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/BackendsStarter.java @@ -17,9 +17,6 @@ import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.CompositeCloseable; -import io.servicetalk.data.jackson.JacksonSerializationProvider; -import io.servicetalk.http.api.HttpSerializationProvider; -import io.servicetalk.http.api.HttpSerializationProviders; import io.servicetalk.transport.api.IoExecutor; import io.servicetalk.transport.api.ServerContext; @@ -55,12 +52,6 @@ public static void main(String[] args) throws Exception { // Shared IoExecutor for the application. IoExecutor ioExecutor = resources.prepend(createIoExecutor()); - // Use Jackson for serialization and deserialization. - // HttpSerializer validates HTTP metadata for serialization/deserialization and also provides higher level - // HTTP focused serialization APIs. - HttpSerializationProvider httpSerializer = HttpSerializationProviders - .jsonSerializer(new JacksonSerializationProvider()); - // This is a single Completable used to await closing of all backends started by this class. It is used to // provide a way to not let main() exit. Completable allServicesOnClose = completed(); @@ -68,22 +59,19 @@ public static void main(String[] args) throws Exception { BackendStarter starter = new BackendStarter(ioExecutor, resources); final ServerContext recommendationService = starter.start(RECOMMENDATIONS_BACKEND_ADDRESS.port(), RECOMMENDATION_SERVICE_NAME, - newRecommendationsService(httpSerializer)); + newRecommendationsService()); allServicesOnClose = allServicesOnClose.merge(recommendationService.onClose()); final ServerContext metadataService = - starter.start(METADATA_BACKEND_ADDRESS.port(), METADATA_SERVICE_NAME, - newMetadataService(httpSerializer)); + starter.start(METADATA_BACKEND_ADDRESS.port(), METADATA_SERVICE_NAME, newMetadataService()); allServicesOnClose = allServicesOnClose.merge(metadataService.onClose()); final ServerContext userService = - starter.start(USER_BACKEND_ADDRESS.port(), USER_SERVICE_NAME, - newUserService(httpSerializer)); + starter.start(USER_BACKEND_ADDRESS.port(), USER_SERVICE_NAME, newUserService()); allServicesOnClose = allServicesOnClose.merge(userService.onClose()); final ServerContext ratingService = - starter.start(RATINGS_BACKEND_ADDRESS.port(), RATING_SERVICE_NAME, - newRatingService(httpSerializer)); + starter.start(RATINGS_BACKEND_ADDRESS.port(), RATING_SERVICE_NAME, newRatingService()); allServicesOnClose = allServicesOnClose.merge(ratingService.onClose()); // Await termination of all backends started by this class. diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/MetadataBackend.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/MetadataBackend.java index b8c2fd1996..0e308cacac 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/MetadataBackend.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/MetadataBackend.java @@ -20,27 +20,20 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpService; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpService; import io.servicetalk.http.router.predicate.HttpPredicateRouterBuilder; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.ENTITY_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.METADATA_SERIALIZER; import static io.servicetalk.examples.http.service.composition.backends.StringUtils.randomString; /** * A service that returns {@link Metadata}s for an entity. */ final class MetadataBackend implements HttpService { - - private static final String ENTITY_ID_QP_NAME = "entityId"; - private final HttpSerializationProvider serializer; - - private MetadataBackend(HttpSerializationProvider serializer) { - this.serializer = serializer; - } - @Override public Single handle(HttpServiceContext ctx, HttpRequest request, HttpResponseFactory responseFactory) { @@ -51,13 +44,13 @@ public Single handle(HttpServiceContext ctx, HttpRequest request, // Create random names and author for the metadata Metadata metadata = new Metadata(entityId, randomString(15), randomString(5)); - return succeeded(responseFactory.ok().payloadBody(metadata, serializer.serializerFor(Metadata.class))); + return succeeded(responseFactory.ok().payloadBody(metadata, METADATA_SERIALIZER)); } - static StreamingHttpService newMetadataService(HttpSerializationProvider serializer) { + static StreamingHttpService newMetadataService() { HttpPredicateRouterBuilder routerBuilder = new HttpPredicateRouterBuilder(); return routerBuilder.whenPathStartsWith("/metadata") - .thenRouteTo(new MetadataBackend(serializer)) + .thenRouteTo(new MetadataBackend()) .buildStreaming(); } } diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RatingBackend.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RatingBackend.java index f4006f7cb9..52f85d31a6 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RatingBackend.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RatingBackend.java @@ -20,7 +20,6 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpService; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpService; @@ -29,19 +28,13 @@ import java.util.concurrent.ThreadLocalRandom; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.ENTITY_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RATING_SERIALIZER; /** * A service that returns {@link Rating}s for an entity. */ final class RatingBackend implements HttpService { - - private static final String ENTITY_ID_QP_NAME = "entityId"; - private final HttpSerializationProvider serializer; - - private RatingBackend(HttpSerializationProvider serializer) { - this.serializer = serializer; - } - @Override public Single handle(HttpServiceContext ctx, HttpRequest request, HttpResponseFactory responseFactory) { @@ -52,13 +45,13 @@ public Single handle(HttpServiceContext ctx, HttpRequest request, // Create a random rating Rating rating = new Rating(entityId, ThreadLocalRandom.current().nextInt(1, 6)); - return succeeded(responseFactory.ok().payloadBody(rating, serializer.serializerFor(Rating.class))); + return succeeded(responseFactory.ok().payloadBody(rating, RATING_SERIALIZER)); } - static StreamingHttpService newRatingService(HttpSerializationProvider serializer) { + static StreamingHttpService newRatingService() { HttpPredicateRouterBuilder routerBuilder = new HttpPredicateRouterBuilder(); return routerBuilder.whenPathStartsWith("/rating") - .thenRouteTo(new RatingBackend(serializer)) + .thenRouteTo(new RatingBackend()) .buildStreaming(); } } diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RecommendationBackend.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RecommendationBackend.java index 62a2ff2b6c..8ebde9cce4 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RecommendationBackend.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/RecommendationBackend.java @@ -21,7 +21,6 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpService; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpRequest; @@ -29,7 +28,6 @@ import io.servicetalk.http.api.StreamingHttpResponseFactory; import io.servicetalk.http.api.StreamingHttpService; import io.servicetalk.http.router.predicate.HttpPredicateRouterBuilder; -import io.servicetalk.serialization.api.TypeHolder; import java.util.ArrayList; import java.util.List; @@ -37,6 +35,9 @@ import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RECOMMEND_LIST_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.RECOMMEND_STREAM_SERIALIZER; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_ID_QP_NAME; import static java.lang.Integer.parseInt; import static java.lang.String.valueOf; import static java.util.concurrent.TimeUnit.SECONDS; @@ -45,22 +46,18 @@ * A service that generates {@link Recommendation}s for a user. */ final class RecommendationBackend { - - private static final TypeHolder> typeOfRecommendation = - new TypeHolder>() { }; - private static final String USER_ID_QP_NAME = "userId"; private static final String EXPECTED_ENTITY_COUNT_QP_NAME = "expectedEntityCount"; private RecommendationBackend() { // No instances. } - static StreamingHttpService newRecommendationsService(HttpSerializationProvider serializer) { + static StreamingHttpService newRecommendationsService() { HttpPredicateRouterBuilder routerBuilder = new HttpPredicateRouterBuilder(); routerBuilder.whenPathStartsWith("/recommendations/stream") - .thenRouteTo(new StreamingService(serializer)); + .thenRouteTo(new StreamingService()); routerBuilder.whenPathStartsWith("/recommendations/aggregated") - .thenRouteTo(new AggregatedService(serializer)); + .thenRouteTo(new AggregatedService()); return routerBuilder.buildStreaming(); } @@ -71,13 +68,6 @@ private static Recommendation newRecommendation(final String userId) { } private static final class StreamingService implements StreamingHttpService { - - private final HttpSerializationProvider serializer; - - StreamingService(final HttpSerializationProvider serializer) { - this.serializer = serializer; - } - @Override public Single handle(HttpServiceContext ctx, StreamingHttpRequest request, StreamingHttpResponseFactory responseFactory) { @@ -97,19 +87,11 @@ public Single handle(HttpServiceContext ctx, StreamingHtt // they are available. .repeat(count -> true); - return succeeded(responseFactory.ok() - .payloadBody(recommendations, serializer.serializerFor(Recommendation.class))); + return succeeded(responseFactory.ok().payloadBody(recommendations, RECOMMEND_STREAM_SERIALIZER)); } } private static final class AggregatedService implements HttpService { - - private final HttpSerializationProvider serializer; - - AggregatedService(final HttpSerializationProvider serializer) { - this.serializer = serializer; - } - @Override public Single handle(HttpServiceContext ctx, HttpRequest request, HttpResponseFactory responseFactory) { @@ -128,8 +110,7 @@ public Single handle(HttpServiceContext ctx, HttpRequest request, } // Serialize the Recommendation list to a single Buffer containing JSON and use it as the response payload. - return succeeded(responseFactory.ok() - .payloadBody(recommendations, serializer.serializerFor(typeOfRecommendation))); + return succeeded(responseFactory.ok().payloadBody(recommendations, RECOMMEND_LIST_SERIALIZER)); } } } diff --git a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/UserBackend.java b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/UserBackend.java index a64343368b..2e83c1d1f4 100644 --- a/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/UserBackend.java +++ b/servicetalk-examples/http/service-composition/src/main/java/io/servicetalk/examples/http/service/composition/backends/UserBackend.java @@ -20,27 +20,20 @@ import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpService; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpService; import io.servicetalk.http.router.predicate.HttpPredicateRouterBuilder; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_ID_QP_NAME; +import static io.servicetalk.examples.http.service.composition.SerializerUtils.USER_SERIALIZER; import static io.servicetalk.examples.http.service.composition.backends.StringUtils.randomString; /** * A service that returns {@link User} for an entity. */ final class UserBackend implements HttpService { - - private static final String USER_ID_QP_NAME = "userId"; - private final HttpSerializationProvider serializer; - - private UserBackend(HttpSerializationProvider serializer) { - this.serializer = serializer; - } - @Override public Single handle(HttpServiceContext ctx, HttpRequest request, HttpResponseFactory responseFactory) { @@ -51,13 +44,13 @@ public Single handle(HttpServiceContext ctx, HttpRequest request, // Create a random rating User user = new User(userId, randomString(5), randomString(3)); - return succeeded(responseFactory.ok().payloadBody(user, serializer.serializerFor(User.class))); + return succeeded(responseFactory.ok().payloadBody(user, USER_SERIALIZER)); } - static StreamingHttpService newUserService(HttpSerializationProvider serializer) { + static StreamingHttpService newUserService() { HttpPredicateRouterBuilder routerBuilder = new HttpPredicateRouterBuilder(); return routerBuilder.whenPathStartsWith("/user") - .thenRouteTo(new UserBackend(serializer)) + .thenRouteTo(new UserBackend()) .buildStreaming(); } } diff --git a/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutClient.java b/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutClient.java index de1a3138f5..37e9fff443 100644 --- a/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutClient.java +++ b/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutClient.java @@ -15,17 +15,17 @@ */ package io.servicetalk.examples.http.timeout; +import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.HttpClient; -import io.servicetalk.http.api.SingleAddressHttpClientBuilder; +import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.netty.HttpClients; import io.servicetalk.http.utils.TimeoutHttpRequesterFilter; -import io.servicetalk.transport.api.HostAndPort; -import java.net.InetSocketAddress; import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.concurrent.api.Single.collectUnorderedDelayError; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; +import static java.time.Duration.ofSeconds; /** * Extends the async 'Hello World!' example to demonstrate use of timeout filters and timeout operators. If a single @@ -36,42 +36,36 @@ * operator. */ public final class TimeoutClient { - public static void main(String[] args) throws Exception { - SingleAddressHttpClientBuilder builder = - HttpClients.forSingleAddress("localhost", 8080) - // Filter enforces that requests made with this client must fully complete - // within 10 seconds or will be cancelled. - .appendClientFilter(new TimeoutHttpRequesterFilter(Duration.ofSeconds(10), true)); - - try (HttpClient client = builder.build()) { - // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting - // before the response has been processed. This isn't typical usage for a streaming API but is useful for - // demonstration purposes. - CountDownLatch responseProcessedLatch = new CountDownLatch(2); - + try (HttpClient client = HttpClients.forSingleAddress("localhost", 8080) + // Filter enforces that requests made with this client must fully complete + // within 10 seconds or will be cancelled. + .appendClientFilter(new TimeoutHttpRequesterFilter(ofSeconds(10), true)) + .build()) { // first request, with default timeout from HttpClient (this will succeed) - client.request(client.get("/sayHello")) - .afterFinally(responseProcessedLatch::countDown) - .afterOnError(System.err::println) - .subscribe(resp -> { + Single respSingle1 = client.request(client.get("/defaultTimeout")) + .whenOnError(System.err::println) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); + System.out.println(resp.payloadBody(textSerializerUtf8())); }); // second request, with custom timeout that is lower than the client default (this will timeout) - client.request(client.get("/sayHello")) + Single respSingle2 = client.request(client.get("/3secondTimeout")) // This request and response must complete within 3 seconds or the request will be cancelled. - .timeout(Duration.ofSeconds(3)) - .afterFinally(responseProcessedLatch::countDown) - .afterOnError(System.err::println) - .subscribe(resp -> { + .timeout(ofSeconds(3)) + .whenOnError(System.err::println) + .whenOnSuccess(resp -> { System.out.println(resp.toString((name, value) -> value)); - System.out.println(resp.payloadBody(textDeserializer())); + System.out.println(resp.payloadBody(textSerializerUtf8())); }); - // block until requests are complete and afterFinally() has been called - responseProcessedLatch.await(); + // Issue the requests in parallel. + collectUnorderedDelayError(respSingle1, respSingle2) + // This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting + // before the response has been processed. This isn't typical usage for an asynchronous API but is useful + // for demonstration purposes. + .toFuture().get(); } } } diff --git a/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutServer.java b/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutServer.java index de49711da2..6b208ecfa2 100644 --- a/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutServer.java +++ b/servicetalk-examples/http/timeout/src/main/java/io/servicetalk/examples/http/timeout/TimeoutServer.java @@ -15,15 +15,12 @@ */ package io.servicetalk.examples.http.timeout; -import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.netty.HttpServers; import io.servicetalk.http.utils.TimeoutHttpServiceFilter; -import java.time.Duration; -import java.util.concurrent.TimeUnit; - import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; +import static java.time.Duration.ofSeconds; /** * Extends the async 'Hello World!' example to demonstrate use of timeout filter. @@ -32,21 +29,12 @@ public final class TimeoutServer { public static void main(String[] args) throws Exception { HttpServers.forPort(8080) // Filter enforces that responses must complete within 30 seconds or will be cancelled. - .appendServiceFilter(new TimeoutHttpServiceFilter(Duration.ofSeconds(30), true)) + .appendServiceFilter(new TimeoutHttpServiceFilter(ofSeconds(30), true)) .listenAndAwait((ctx, request, responseFactory) -> - Single.defer(() -> { // Force a 5 second delay in the response. - try { - TimeUnit.SECONDS.sleep(5); - } catch (InterruptedException woken) { - Thread.interrupted(); - // just continue - } - - return succeeded(responseFactory.ok() - .payloadBody("Hello World!", textSerializer())) - .subscribeShareContext(); - })) + ctx.executionContext().executor().timer(ofSeconds(5)) + .concat(succeeded(responseFactory.ok() + .payloadBody("Hello World!", textSerializerUtf8())))) .awaitShutdown(); } } diff --git a/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsClient.java b/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsClient.java index a6e964cbfb..9d3243e0b1 100644 --- a/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsClient.java +++ b/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsClient.java @@ -20,7 +20,7 @@ import io.servicetalk.http.netty.HttpClients; import static io.servicetalk.examples.http.uds.blocking.UdsUtils.udsAddress; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * AF_UNIX socket client example. @@ -30,7 +30,7 @@ public static void main(String[] args) throws Exception { try (BlockingHttpClient client = HttpClients.forResolvedAddress(udsAddress()).buildBlocking()) { HttpResponse response = client.request(client.get("/sayHello")); System.out.println(response.toString((name, value) -> value)); - System.out.println(response.payloadBody(textDeserializer())); + System.out.println(response.payloadBody(textSerializerUtf8())); } } } diff --git a/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsServer.java b/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsServer.java index d01a285bd0..7066251a71 100644 --- a/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsServer.java +++ b/servicetalk-examples/http/uds/src/main/java/io/servicetalk/examples/http/uds/blocking/BlockingUdsServer.java @@ -21,7 +21,7 @@ import java.io.File; import static io.servicetalk.examples.http.uds.blocking.UdsUtils.udsAddress; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; /** * AF_UNIX socket server example. @@ -39,7 +39,7 @@ public static void main(String[] args) throws Exception { HttpServers.forAddress(udsAddress) .listenBlockingAndAwait((ctx, request, responseFactory) -> - responseFactory.ok().payloadBody("Hello World!", textSerializer())) + responseFactory.ok().payloadBody("Hello World!", textSerializerUtf8())) .awaitShutdown(); } } diff --git a/servicetalk-grpc-api/build.gradle b/servicetalk-grpc-api/build.gradle index 4a927c90a4..703b679a5c 100644 --- a/servicetalk-grpc-api/build.gradle +++ b/servicetalk-grpc-api/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation project(":servicetalk-utils-internal") implementation project(":servicetalk-grpc-internal") implementation project(":servicetalk-oio-api-internal") + implementation project(":servicetalk-serializer-utils") implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "com.google.code.findbugs:jsr305:$jsr305Version" implementation "com.google.protobuf:protobuf-java:$protobufVersion" diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientCallFactory.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientCallFactory.java index e84b47141c..2a1d36bec1 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientCallFactory.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientCallFactory.java @@ -19,33 +19,45 @@ import io.servicetalk.concurrent.api.AsyncContext; import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Publisher; -import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.grpc.api.GrpcUtils.DefaultMethodDescriptor; import io.servicetalk.http.api.BlockingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpClient; import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpRequest; -import io.servicetalk.http.api.HttpRequestFactory; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.api.StreamingHttpRequest; import java.time.Duration; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; import static io.servicetalk.concurrent.internal.BlockingIterables.singletonBlockingIterable; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.grpc.api.GrpcUtils.GRPC_PROTO_CONTENT_TYPE; +import static io.servicetalk.grpc.api.GrpcUtils.decompressors; +import static io.servicetalk.grpc.api.GrpcUtils.defaultToInt; +import static io.servicetalk.grpc.api.GrpcUtils.grpcContentType; import static io.servicetalk.grpc.api.GrpcUtils.initRequest; -import static io.servicetalk.grpc.api.GrpcUtils.readGrpcMessageEncoding; +import static io.servicetalk.grpc.api.GrpcUtils.readGrpcMessageEncodingRaw; +import static io.servicetalk.grpc.api.GrpcUtils.serializerDeserializer; import static io.servicetalk.grpc.api.GrpcUtils.toGrpcException; -import static io.servicetalk.grpc.api.GrpcUtils.uncheckedCast; import static io.servicetalk.grpc.api.GrpcUtils.validateResponseAndGetPayload; import static io.servicetalk.grpc.internal.DeadlineUtils.GRPC_DEADLINE_KEY; import static java.util.Objects.requireNonNull; final class DefaultGrpcClientCallFactory implements GrpcClientCallFactory { - + private static final String UNKNOWN_PATH = ""; + private static final Map> serializerMap = new ConcurrentHashMap<>(2); + private static final Map> streamingSerializerMap = + new ConcurrentHashMap<>(2); private final StreamingHttpClient streamingHttpClient; private final GrpcExecutionContext executionContext; @Nullable @@ -58,134 +70,253 @@ final class DefaultGrpcClientCallFactory implements GrpcClientCallFactory { this.defaultTimeout = defaultTimeout; } + @Deprecated @Override - public ClientCall - newCall(final GrpcSerializationProvider serializationProvider, + public ClientCall newCall(final GrpcSerializationProvider serializationProvider, final Class requestClass, final Class responseClass) { - requireNonNull(serializationProvider); - requireNonNull(requestClass); - requireNonNull(responseClass); + return newCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + + @Override + public ClientCall newCall(final MethodDescriptor methodDescriptor, + final BufferDecoderGroup decompressors) { final HttpClient client = streamingHttpClient.asClient(); - final List supportedCodings = serializationProvider.supportedMessageCodings(); + GrpcSerializer serializerIdentity = serializer(methodDescriptor); + GrpcDeserializer deserializerIdentity = deserializer(methodDescriptor); + List> deserializers = deserializers(methodDescriptor, decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); return (metadata, request) -> { Duration timeout = timeoutForRequest(metadata.timeout()); - final HttpRequest httpRequest = newAggregatedRequest(metadata, request, client, - serializationProvider, supportedCodings, timeout, requestClass); + GrpcSerializer serializer = serializer(methodDescriptor, serializerIdentity, + metadata.requestCompressor()); + String mdPath = methodDescriptor.httpPath(); + HttpRequest httpRequest = client.post(UNKNOWN_PATH.equals(mdPath) ? metadata.path() : mdPath); + initRequest(httpRequest, requestContentType, serializer.messageEncoding(), acceptedEncoding, timeout); + httpRequest.payloadBody(serializer.serialize(request, client.executionContext().bufferAllocator())); @Nullable final GrpcExecutionStrategy strategy = metadata.strategy(); return (strategy == null ? client.request(httpRequest) : client.request(strategy, httpRequest)) - .map(response -> validateResponseAndGetPayload(response, serializationProvider.deserializerFor( - readGrpcMessageEncoding(response, supportedCodings), responseClass))) + .map(response -> validateResponseAndGetPayload(response, responseContentType, + client.executionContext().bufferAllocator(), readGrpcMessageEncodingRaw(response.headers(), + deserializerIdentity, deserializers, GrpcDeserializer::messageEncoding))) .onErrorMap(GrpcUtils::toGrpcException); }; } + @Deprecated + @Override + public StreamingClientCall newStreamingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newStreamingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + true, true, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + @Override - public StreamingClientCall - newStreamingCall(final GrpcSerializationProvider serializationProvider, final Class requestClass, - final Class responseClass) { - requireNonNull(serializationProvider); - requireNonNull(requestClass); - requireNonNull(responseClass); - final List supportedCodings = serializationProvider.supportedMessageCodings(); + public StreamingClientCall newStreamingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { + GrpcStreamingSerializer serializerIdentity = streamingSerializer(methodDescriptor); + GrpcStreamingDeserializer deserializerIdentity = streamingDeserializer(methodDescriptor); + List> deserializers = streamingDeserializers(methodDescriptor, + decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); return (metadata, request) -> { - final StreamingHttpRequest httpRequest = streamingHttpClient.post(metadata.path()); Duration timeout = timeoutForRequest(metadata.timeout()); - initRequest(httpRequest, supportedCodings, timeout); - httpRequest.payloadBody(request.map(GrpcUtils::uncheckedCast), - serializationProvider.serializerFor(metadata.requestEncoding(), requestClass)); + GrpcStreamingSerializer serializer = streamingSerializer(methodDescriptor, serializerIdentity, + metadata.requestCompressor()); + String mdPath = methodDescriptor.httpPath(); + StreamingHttpRequest httpRequest = streamingHttpClient.post(UNKNOWN_PATH.equals(mdPath) ? + metadata.path() : mdPath); + initRequest(httpRequest, requestContentType, serializer.messageEncoding(), acceptedEncoding, timeout); + httpRequest.payloadBody(serializer.serialize(request, + streamingHttpClient.executionContext().bufferAllocator())); @Nullable final GrpcExecutionStrategy strategy = metadata.strategy(); return (strategy == null ? streamingHttpClient.request(httpRequest) : streamingHttpClient.request(strategy, httpRequest)) - .flatMapPublisher(response -> validateResponseAndGetPayload(response, - serializationProvider.deserializerFor( - readGrpcMessageEncoding(response, supportedCodings), responseClass))) + .flatMapPublisher(response -> validateResponseAndGetPayload(response, responseContentType, + streamingHttpClient.executionContext().bufferAllocator(), readGrpcMessageEncodingRaw( + response.headers(), deserializerIdentity, deserializers, + GrpcStreamingDeserializer::messageEncoding))) .onErrorMap(GrpcUtils::toGrpcException); }; } + @Deprecated + @Override + public RequestStreamingClientCall newRequestStreamingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newRequestStreamingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + true, true, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + @Override - public RequestStreamingClientCall - newRequestStreamingCall(final GrpcSerializationProvider serializationProvider, - final Class requestClass, final Class responseClass) { + public RequestStreamingClientCall newRequestStreamingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { final StreamingClientCall streamingClientCall = - newStreamingCall(serializationProvider, requestClass, responseClass); + newStreamingCall(methodDescriptor, decompressors); return (metadata, request) -> streamingClientCall.request(metadata, request).firstOrError(); } + @Deprecated + @Override + public ResponseStreamingClientCall newResponseStreamingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newResponseStreamingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + @Override - public ResponseStreamingClientCall - newResponseStreamingCall(final GrpcSerializationProvider serializationProvider, - final Class requestClass, final Class responseClass) { + public ResponseStreamingClientCall newResponseStreamingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { final StreamingClientCall streamingClientCall = - newStreamingCall(serializationProvider, requestClass, responseClass); + newStreamingCall(methodDescriptor, decompressors); return (metadata, request) -> streamingClientCall.request(metadata, Publisher.from(request)); } + @Deprecated + @Override + public BlockingClientCall newBlockingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newBlockingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + @Override - public BlockingClientCall - newBlockingCall(final GrpcSerializationProvider serializationProvider, - final Class requestClass, final Class responseClass) { - requireNonNull(serializationProvider); - requireNonNull(requestClass); - requireNonNull(responseClass); - final List supportedCodings = serializationProvider.supportedMessageCodings(); + public BlockingClientCall newBlockingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { final BlockingHttpClient client = streamingHttpClient.asBlockingClient(); + GrpcSerializer serializerIdentity = serializer(methodDescriptor); + GrpcDeserializer deserializerIdentity = deserializer(methodDescriptor); + List> deserializers = deserializers(methodDescriptor, decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); return (metadata, request) -> { Duration timeout = timeoutForRequest(metadata.timeout()); - final HttpRequest httpRequest = newAggregatedRequest(metadata, request, client, - serializationProvider, supportedCodings, timeout, requestClass); - @Nullable - final GrpcExecutionStrategy strategy = metadata.strategy(); + GrpcSerializer serializer = serializer(methodDescriptor, serializerIdentity, + metadata.requestCompressor()); + String mdPath = methodDescriptor.httpPath(); + HttpRequest httpRequest = client.post(UNKNOWN_PATH.equals(mdPath) ? metadata.path() : mdPath); + initRequest(httpRequest, requestContentType, serializer.messageEncoding(), acceptedEncoding, timeout); + httpRequest.payloadBody(serializer.serialize(request, client.executionContext().bufferAllocator())); try { + @Nullable + final GrpcExecutionStrategy strategy = metadata.strategy(); final HttpResponse response = strategy == null ? client.request(httpRequest) : client.request(strategy, httpRequest); - return validateResponseAndGetPayload(response, - serializationProvider.deserializerFor( - readGrpcMessageEncoding(response, supportedCodings), responseClass)); - } catch (Exception all) { - throw toGrpcException(all); + return validateResponseAndGetPayload(response, responseContentType, + client.executionContext().bufferAllocator(), readGrpcMessageEncodingRaw(response.headers(), + deserializerIdentity, deserializers, GrpcDeserializer::messageEncoding)); + } catch (Throwable cause) { + throw toGrpcException(cause); } }; } + @Deprecated @Override - public BlockingStreamingClientCall - newBlockingStreamingCall(final GrpcSerializationProvider serializationProvider, - final Class requestClass, final Class responseClass) { - requireNonNull(serializationProvider); - requireNonNull(requestClass); - requireNonNull(responseClass); + public BlockingStreamingClientCall newBlockingStreamingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newBlockingStreamingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + true, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + + @Override + public BlockingStreamingClientCall newBlockingStreamingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { + GrpcStreamingSerializer serializerIdentity = streamingSerializer(methodDescriptor); + GrpcStreamingDeserializer deserializerIdentity = streamingDeserializer(methodDescriptor); + List> deserializers = streamingDeserializers(methodDescriptor, + decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); final BlockingStreamingHttpClient client = streamingHttpClient.asBlockingStreamingClient(); - final List supportedCodings = serializationProvider.supportedMessageCodings(); return (metadata, request) -> { - final BlockingStreamingHttpRequest httpRequest = client.post(metadata.path()); Duration timeout = timeoutForRequest(metadata.timeout()); - initRequest(httpRequest, supportedCodings, timeout); - httpRequest.payloadBody(request, serializationProvider - .serializerFor(metadata.requestEncoding(), requestClass)); - @Nullable - final GrpcExecutionStrategy strategy = metadata.strategy(); + GrpcStreamingSerializer serializer = streamingSerializer(methodDescriptor, serializerIdentity, + metadata.requestCompressor()); + String mdPath = methodDescriptor.httpPath(); + BlockingStreamingHttpRequest httpRequest = client.post(UNKNOWN_PATH.equals(mdPath) ? + metadata.path() : mdPath); + initRequest(httpRequest, requestContentType, serializer.messageEncoding(), acceptedEncoding, timeout); + httpRequest.payloadBody(serializer.serialize(request, + streamingHttpClient.executionContext().bufferAllocator())); try { + @Nullable + final GrpcExecutionStrategy strategy = metadata.strategy(); final BlockingStreamingHttpResponse response = strategy == null ? client.request(httpRequest) : client.request(strategy, httpRequest); - return validateResponseAndGetPayload(response.toStreamingResponse(), - serializationProvider.deserializerFor( - readGrpcMessageEncoding(response, supportedCodings), responseClass)) - .onErrorMap(GrpcUtils::toGrpcException).toIterable(); - } catch (Exception all) { - throw toGrpcException(all); + return validateResponseAndGetPayload(response.toStreamingResponse(), responseContentType, + client.executionContext().bufferAllocator(), readGrpcMessageEncodingRaw( + response.headers(), deserializerIdentity, deserializers, + GrpcStreamingDeserializer::messageEncoding)).toIterable(); + } catch (Throwable cause) { + throw toGrpcException(cause); } }; } + @Deprecated @Override - public BlockingRequestStreamingClientCall - newBlockingRequestStreamingCall(final GrpcSerializationProvider serializationProvider, - final Class requestClass, final Class responseClass) { + public BlockingRequestStreamingClientCall newBlockingRequestStreamingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newBlockingRequestStreamingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + true, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + + @Override + public BlockingRequestStreamingClientCall newBlockingRequestStreamingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { final BlockingStreamingClientCall streamingClientCall = - newBlockingStreamingCall(serializationProvider, requestClass, responseClass); + newBlockingStreamingCall(methodDescriptor, decompressors); return (metadata, request) -> { try (BlockingIterator iterator = streamingClientCall.request(metadata, request).iterator()) { final Resp firstItem = iterator.next(); @@ -199,12 +330,24 @@ final class DefaultGrpcClientCallFactory implements GrpcClientCallFactory { }; } + @Deprecated + @Override + public BlockingResponseStreamingClientCall newBlockingResponseStreamingCall( + final GrpcSerializationProvider serializationProvider, final Class requestClass, + final Class responseClass) { + return newBlockingResponseStreamingCall(new DefaultMethodDescriptor<>(UNKNOWN_PATH, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings())); + } + @Override - public BlockingResponseStreamingClientCall - newBlockingResponseStreamingCall(final GrpcSerializationProvider serializationProvider, - final Class requestClass, final Class responseClass) { + public BlockingResponseStreamingClientCall newBlockingResponseStreamingCall( + final MethodDescriptor methodDescriptor, final BufferDecoderGroup decompressors) { final BlockingStreamingClientCall streamingClientCall = - newBlockingStreamingCall(serializationProvider, requestClass, responseClass); + newBlockingStreamingCall(methodDescriptor, decompressors); return (metadata, request) -> streamingClientCall.request(metadata, singletonBlockingIterable(request)); } @@ -228,17 +371,6 @@ public Completable onClose() { return streamingHttpClient.onClose(); } - private HttpRequest newAggregatedRequest(final GrpcClientMetadata metadata, final Req rawReq, - final HttpRequestFactory requestFactory, - final GrpcSerializationProvider serializationProvider, - final List supportedCodings, - final Duration timeout, final Class requestClass) { - final HttpRequest httpRequest = requestFactory.post(metadata.path()); - initRequest(httpRequest, supportedCodings, timeout); - return httpRequest.payloadBody(uncheckedCast(rawReq), - serializationProvider.serializerFor(metadata.requestEncoding(), requestClass)); - } - /** * Determines the timeout for a new request using three potential sources; the deadline in the async context, the * request timeout, and the client default. The timeout will be the lesser of the context and request timeouts or if @@ -260,4 +392,59 @@ private HttpRequest newAggregatedRequest(final GrpcClientMetadata metadata return null != timeout ? timeout : defaultTimeout; } + + private static List> streamingDeserializers( + MethodDescriptor methodDescriptor, List decompressors) { + return GrpcUtils.streamingDeserializers( + methodDescriptor.responseDescriptor().serializerDescriptor().serializer(), decompressors); + } + + private static GrpcStreamingSerializer streamingSerializer( + MethodDescriptor methodDescriptor) { + return new GrpcStreamingSerializer<>( + methodDescriptor.requestDescriptor().serializerDescriptor().bytesEstimator(), + methodDescriptor.requestDescriptor().serializerDescriptor().serializer()); + } + + private static GrpcStreamingDeserializer streamingDeserializer( + MethodDescriptor methodDescriptor) { + return new GrpcStreamingDeserializer<>( + methodDescriptor.responseDescriptor().serializerDescriptor().serializer()); + } + + private static List> deserializers( + MethodDescriptor methodDescriptor, List decompressors) { + return GrpcUtils.deserializers( + methodDescriptor.responseDescriptor().serializerDescriptor().serializer(), decompressors); + } + + private static GrpcSerializer serializer(MethodDescriptor methodDescriptor) { + return new GrpcSerializer<>(methodDescriptor.requestDescriptor().serializerDescriptor().bytesEstimator(), + methodDescriptor.requestDescriptor().serializerDescriptor().serializer()); + } + + @SuppressWarnings("unchecked") + private static GrpcSerializer serializer( + MethodDescriptor methodDescriptor, GrpcSerializer serializerIdentity, + @Nullable BufferEncoder compressor) { + return compressor == null || compressor == identityEncoder() ? serializerIdentity : + (GrpcSerializer) serializerMap.computeIfAbsent(compressor, key -> new GrpcSerializer<>( + methodDescriptor.requestDescriptor().serializerDescriptor().bytesEstimator(), + methodDescriptor.requestDescriptor().serializerDescriptor().serializer(), key)); + } + + @SuppressWarnings("unchecked") + private static GrpcStreamingSerializer streamingSerializer( + MethodDescriptor methodDescriptor, GrpcStreamingSerializer serializerIdentity, + @Nullable BufferEncoder compressor) { + return compressor == null || compressor == identityEncoder() ? serializerIdentity : + (GrpcStreamingSerializer) streamingSerializerMap.computeIfAbsent(compressor, key -> + new GrpcStreamingSerializer<>( + methodDescriptor.requestDescriptor().serializerDescriptor().bytesEstimator(), + methodDescriptor.requestDescriptor().serializerDescriptor().serializer(), key)); + } + + private static GrpcDeserializer deserializer(MethodDescriptor methodDescriptor) { + return new GrpcDeserializer<>(methodDescriptor.responseDescriptor().serializerDescriptor().serializer()); + } } diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientMetadata.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientMetadata.java index e13ddc9202..ce849cd19c 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientMetadata.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcClientMetadata.java @@ -15,25 +15,30 @@ */ package io.servicetalk.grpc.api; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.encoding.api.internal.ContentCodecToBufferEncoder; import java.time.Duration; -import java.util.Objects; import javax.annotation.Nullable; import static io.servicetalk.encoding.api.Identity.identity; import static io.servicetalk.grpc.internal.DeadlineUtils.GRPC_MAX_TIMEOUT; import static io.servicetalk.utils.internal.DurationUtils.ensurePositive; import static io.servicetalk.utils.internal.DurationUtils.isInfinite; +import static java.util.Objects.requireNonNull; /** * Default implementation for {@link DefaultGrpcClientMetadata}. */ public class DefaultGrpcClientMetadata extends DefaultGrpcMetadata implements GrpcClientMetadata { - + private static final String UNKNOWN_PATH = ""; + public static final GrpcClientMetadata INSTANCE = new DefaultGrpcClientMetadata(); @Nullable private final GrpcExecutionStrategy strategy; - + @Nullable + private final BufferEncoder requestEncoder; + @Deprecated private final ContentCodec requestEncoding; /** @@ -42,51 +47,84 @@ public class DefaultGrpcClientMetadata extends DefaultGrpcMetadata implements Gr @Nullable private final Duration timeout; + private DefaultGrpcClientMetadata() { + this((GrpcExecutionStrategy) null, null, null); + } + /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata()}. * @param path for the associated gRPC method. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path) { - this(path, (GrpcExecutionStrategy) null, identity(), null); + this(path, null, identity(), null); } /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(BufferEncoder)} * @param path for the associated gRPC method. * @param requestEncoding {@link ContentCodec} to use for the associated gRPC * method. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, final ContentCodec requestEncoding) { this(path, null, requestEncoding, null); } + /** + * Creates a new instance. + * @param requestEncoding Used to compress the request. + */ + public DefaultGrpcClientMetadata(final BufferEncoder requestEncoding) { + this(null, requestEncoding, null); + } + + /** + * Create a new instance. + * @param timeout A timeout after which the response is no longer wanted. + */ + public DefaultGrpcClientMetadata(final Duration timeout) { + this(null, (BufferEncoder) null, timeout); + } + /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(BufferEncoder, Duration)}. * @param path for the associated gRPC method. * @param requestEncoding {@link ContentCodec} to use for the associated gRPC * method. * @param timeout A timeout after which the response is no longer wanted. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, final ContentCodec requestEncoding, final Duration timeout) { this(path, null, requestEncoding, timeout); } + /** + * Creates a new instance. + * @param requestEncoding Used to compress the request. + * @param timeout A timeout after which the response is no longer wanted. + */ + public DefaultGrpcClientMetadata(final BufferEncoder requestEncoding, final Duration timeout) { + this(null, requestEncoding, timeout); + } + /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(GrpcExecutionStrategy, BufferEncoder)}. * @param path for the associated gRPC method. * @param strategy {@link GrpcExecutionStrategy} to use for the associated gRPC * method. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, @Nullable final GrpcExecutionStrategy strategy) { this(path, strategy, identity(), null); @@ -95,26 +133,39 @@ protected DefaultGrpcClientMetadata(final String path, /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(GrpcExecutionStrategy, BufferEncoder)}. * @param path for the associated gRPC method. * @param strategy {@link GrpcExecutionStrategy} to use for the associated gRPC * method. * @param requestEncoding {@link ContentCodec} to use for the associated gRPC * method. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, @Nullable final GrpcExecutionStrategy strategy, final ContentCodec requestEncoding) { this(path, strategy, requestEncoding, null); } + /** + * Create a new instance. + * @param strategy {@link GrpcExecutionStrategy} to use for the associated gRPC + * call. + * @param requestEncoding Used to compress the request. + */ + public DefaultGrpcClientMetadata(@Nullable final GrpcExecutionStrategy strategy, + final BufferEncoder requestEncoding) { + this(strategy, requestEncoding, null); + } + /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(BufferEncoder, Duration)}. * @param path for the associated gRPC method. * @param timeout A timeout after which the response is no longer wanted. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, @Nullable final Duration timeout) { this(path, null, identity(), timeout); @@ -123,12 +174,13 @@ protected DefaultGrpcClientMetadata(final String path, /** * Creates a new instance using provided parameters and defaults for * {@link #DefaultGrpcClientMetadata(String, GrpcExecutionStrategy, ContentCodec, Duration)}. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(GrpcExecutionStrategy, BufferEncoder, Duration)}. * @param path for the associated gRPC method. * @param strategy {@link GrpcExecutionStrategy} to use for the associated gRPC * method. * @param timeout A timeout after which the response is no longer wanted. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, @Nullable final GrpcExecutionStrategy strategy, @Nullable final Duration timeout) { @@ -137,7 +189,7 @@ protected DefaultGrpcClientMetadata(final String path, /** * Creates a new instance which uses the provided path, execution strategy, content codec, and timeout. - * + * @deprecated Use {@link #DefaultGrpcClientMetadata(GrpcExecutionStrategy, BufferEncoder, Duration)}. * @param path for the associated gRPC method. * @param strategy {@link GrpcExecutionStrategy} to use for the associated gRPC * method. @@ -145,13 +197,63 @@ protected DefaultGrpcClientMetadata(final String path, * method. * @param timeout A timeout after which the response is no longer wanted. */ + @Deprecated protected DefaultGrpcClientMetadata(final String path, @Nullable final GrpcExecutionStrategy strategy, final ContentCodec requestEncoding, @Nullable final Duration timeout) { super(path); this.strategy = strategy; - this.requestEncoding = Objects.requireNonNull(requestEncoding, "requestEncoding"); + this.requestEncoding = requireNonNull(requestEncoding); + this.requestEncoder = requestEncoding == identity() ? null : new ContentCodecToBufferEncoder(requestEncoding); + if (null != timeout) { + ensurePositive(timeout, "timeout"); + } + this.timeout = isInfinite(timeout, GRPC_MAX_TIMEOUT) ? null : timeout; + } + + /** + * Create a new instance. + * @deprecated Use {@link #DefaultGrpcClientMetadata(DefaultGrpcClientMetadata)}. + * @param path for the associated gRPC method. + * @param rhs Copy everything except the path from this object. + */ + @Deprecated + protected DefaultGrpcClientMetadata(final String path, + final GrpcClientMetadata rhs) { + super(path); + this.strategy = rhs.strategy(); + this.requestEncoding = rhs.requestEncoding(); + this.requestEncoder = rhs.requestCompressor(); + this.timeout = rhs.timeout(); + } + + /** + * Create a new instance, by copying an existing instance. + * @param rhs Right hand side to copy from. + */ + protected DefaultGrpcClientMetadata(final DefaultGrpcClientMetadata rhs) { + super(UNKNOWN_PATH); + this.strategy = rhs.strategy; + this.requestEncoding = rhs.requestEncoding(); + this.requestEncoder = rhs.requestCompressor(); + this.timeout = rhs.timeout(); + } + + /** + * Create a new instance. + * @param strategy {@link GrpcExecutionStrategy} to use for the associated gRPC + * call. + * @param requestEncoding Used to compress the request. + * @param timeout A timeout after which the response is no longer wanted. + */ + public DefaultGrpcClientMetadata(@Nullable final GrpcExecutionStrategy strategy, + @Nullable final BufferEncoder requestEncoding, + @Nullable final Duration timeout) { + super(UNKNOWN_PATH); + this.strategy = strategy; + this.requestEncoding = identity(); + this.requestEncoder = requestEncoding; if (null != timeout) { ensurePositive(timeout, "timeout"); } @@ -163,11 +265,18 @@ public final GrpcExecutionStrategy strategy() { return strategy; } + @Deprecated @Override public ContentCodec requestEncoding() { return requestEncoding; } + @Nullable + @Override + public BufferEncoder requestCompressor() { + return requestEncoder; + } + @Override @Nullable public Duration timeout() { diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java index 2a601871c2..d45b6df55c 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java @@ -30,6 +30,7 @@ class DefaultGrpcMetadata implements GrpcMetadata { this.path = requireNonNull(path, "path"); } + @Deprecated @Override public String path() { return path; diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcServiceContext.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcServiceContext.java index 765897c04e..d0c205a23c 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcServiceContext.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcServiceContext.java @@ -23,12 +23,11 @@ import java.net.SocketAddress; import java.net.SocketOption; -import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import javax.net.ssl.SSLSession; -import static java.util.Collections.unmodifiableList; +import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; final class DefaultGrpcServiceContext extends DefaultGrpcMetadata implements GrpcServiceContext { @@ -36,15 +35,15 @@ final class DefaultGrpcServiceContext extends DefaultGrpcMetadata implements Grp private final ConnectionContext connectionContext; private final GrpcExecutionContext executionContext; private final GrpcProtocol protocol; + @Deprecated private final List supportedMessageCodings; - DefaultGrpcServiceContext(final String path, final HttpServiceContext httpServiceContext, - final List supportedMessageCodings) { + DefaultGrpcServiceContext(final String path, final HttpServiceContext httpServiceContext) { super(path); connectionContext = requireNonNull(httpServiceContext); executionContext = new DefaultGrpcExecutionContext(httpServiceContext.executionContext()); protocol = new DefaultGrpcProtocol(httpServiceContext.protocol()); - this.supportedMessageCodings = unmodifiableList(new ArrayList<>(supportedMessageCodings)); + this.supportedMessageCodings = emptyList(); } @Override @@ -68,6 +67,7 @@ public GrpcExecutionContext executionContext() { return executionContext; } + @Deprecated @Override public List supportedMessageCodings() { return supportedMessageCodings; diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientCallFactory.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientCallFactory.java index 31584ae489..f34e71ffdd 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientCallFactory.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientCallFactory.java @@ -19,6 +19,7 @@ import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferDecoderGroup; import io.servicetalk.http.api.StreamingHttpClient; import java.time.Duration; @@ -32,7 +33,7 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { /** * Creates a new {@link ClientCall}. - * + * @deprecated Use {@link #newCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -40,13 +41,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link ClientCall}. */ - ClientCall - newCall(GrpcSerializationProvider serializationProvider, - Class requestClass, Class responseClass); + @Deprecated + ClientCall newCall(GrpcSerializationProvider serializationProvider, + Class requestClass, Class responseClass); + + /** + * Create a new {@link ClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link ClientCall}. + */ + ClientCall newCall(MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors); /** * Creates a new {@link StreamingClientCall}. - * + * @deprecated Use {@link #newStreamingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -54,13 +66,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link StreamingClientCall}. */ - StreamingClientCall - newStreamingCall(GrpcSerializationProvider serializationProvider, Class requestClass, - Class responseClass); + @Deprecated + StreamingClientCall newStreamingCall(GrpcSerializationProvider serializationProvider, + Class requestClass, Class responseClass); + + /** + * Create a new {@link StreamingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link StreamingClientCall}. + */ + StreamingClientCall newStreamingCall(MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors); /** * Creates a new {@link RequestStreamingClientCall}. - * + * @deprecated Use {@link #newRequestStreamingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -68,13 +91,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link RequestStreamingClientCall}. */ - RequestStreamingClientCall - newRequestStreamingCall(GrpcSerializationProvider serializationProvider, Class requestClass, - Class responseClass); + @Deprecated + RequestStreamingClientCall newRequestStreamingCall( + GrpcSerializationProvider serializationProvider, Class requestClass, Class responseClass); + + /** + * Create a new {@link RequestStreamingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link RequestStreamingClientCall}. + */ + RequestStreamingClientCall newRequestStreamingCall( + MethodDescriptor methodDescriptor, BufferDecoderGroup decompressors); /** * Creates a new {@link ResponseStreamingClientCall}. - * + * @deprecated Use {@link #newResponseStreamingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -82,13 +116,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link ResponseStreamingClientCall}. */ - ResponseStreamingClientCall - newResponseStreamingCall(GrpcSerializationProvider serializationProvider, Class requestClass, - Class responseClass); + @Deprecated + ResponseStreamingClientCall newResponseStreamingCall( + GrpcSerializationProvider serializationProvider, Class requestClass, Class responseClass); + + /** + * Create a new {@link ResponseStreamingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link ResponseStreamingClientCall}. + */ + ResponseStreamingClientCall newResponseStreamingCall( + MethodDescriptor methodDescriptor, BufferDecoderGroup decompressors); /** * Creates a new {@link BlockingClientCall}. - * + * @deprecated use {@link #newBlockingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -96,13 +141,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link BlockingClientCall}. */ - BlockingClientCall - newBlockingCall(GrpcSerializationProvider serializationProvider, - Class requestClass, Class responseClass); + @Deprecated + BlockingClientCall newBlockingCall(GrpcSerializationProvider serializationProvider, + Class requestClass, Class responseClass); + + /** + * Create a new {@link BlockingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link BlockingClientCall}. + */ + BlockingClientCall newBlockingCall(MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors); /** * Creates a new {@link BlockingStreamingClientCall}. - * + * @deprecated Use {@link #newBlockingStreamingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -110,13 +166,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link BlockingStreamingClientCall}. */ - BlockingStreamingClientCall - newBlockingStreamingCall(GrpcSerializationProvider serializationProvider, Class requestClass, - Class responseClass); + @Deprecated + BlockingStreamingClientCall newBlockingStreamingCall( + GrpcSerializationProvider serializationProvider, Class requestClass, Class responseClass); + + /** + * Create a new {@link BlockingStreamingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link BlockingStreamingClientCall}. + */ + BlockingStreamingClientCall newBlockingStreamingCall( + MethodDescriptor methodDescriptor, BufferDecoderGroup decompressors); /** * Creates a new {@link BlockingRequestStreamingClientCall}. - * + * @deprecated Use {@link #newBlockingRequestStreamingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -124,13 +191,24 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link BlockingRequestStreamingClientCall}. */ - BlockingRequestStreamingClientCall - newBlockingRequestStreamingCall(GrpcSerializationProvider serializationProvider, Class requestClass, - Class responseClass); + @Deprecated + BlockingRequestStreamingClientCall newBlockingRequestStreamingCall( + GrpcSerializationProvider serializationProvider, Class requestClass, Class responseClass); + + /** + * Create a new {@link BlockingRequestStreamingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link BlockingRequestStreamingClientCall}. + */ + BlockingRequestStreamingClientCall newBlockingRequestStreamingCall( + MethodDescriptor methodDescriptor, BufferDecoderGroup decompressors); /** * Creates a new {@link BlockingResponseStreamingClientCall}. - * + * @deprecated Use {@link #newBlockingResponseStreamingCall(MethodDescriptor, BufferDecoderGroup)}. * @param serializationProvider {@link GrpcSerializationProvider} to use. * @param requestClass {@link Class} object for the request. * @param responseClass {@link Class} object for the response. @@ -138,9 +216,20 @@ public interface GrpcClientCallFactory extends ListenableAsyncCloseable { * @param Type of response. * @return {@link BlockingResponseStreamingClientCall}. */ - BlockingResponseStreamingClientCall - newBlockingResponseStreamingCall(GrpcSerializationProvider serializationProvider, Class requestClass, - Class responseClass); + @Deprecated + BlockingResponseStreamingClientCall newBlockingResponseStreamingCall( + GrpcSerializationProvider serializationProvider, Class requestClass, Class responseClass); + + /** + * Create a new {@link BlockingResponseStreamingClientCall}. + * @param methodDescriptor describes the method characteristics and how to do serialization of individual objects. + * @param decompressors describes the decompression that is supported for this call. + * @param Type of request. + * @param Type of response. + * @return {@link BlockingResponseStreamingClientCall}. + */ + BlockingResponseStreamingClientCall newBlockingResponseStreamingCall( + MethodDescriptor methodDescriptor, BufferDecoderGroup decompressors); /** * Get the {@link GrpcExecutionContext} used during construction of this object. diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientFactory.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientFactory.java index 94d00017a4..93010a52d0 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientFactory.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientFactory.java @@ -15,15 +15,16 @@ */ package io.servicetalk.grpc.api; +import io.servicetalk.encoding.api.BufferDecoderGroup; import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.encoding.api.EmptyBufferDecoderGroup; import io.servicetalk.encoding.api.Identity; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; -import static io.servicetalk.encoding.api.Identity.identity; -import static java.util.Collections.singletonList; +import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; @@ -47,7 +48,9 @@ public abstract class GrpcClientFactory supportedCodings = singletonList(identity()); + @Deprecated + private List supportedCodings = emptyList(); + private BufferDecoderGroup bufferDecoderGroup = EmptyBufferDecoderGroup.INSTANCE; /** * Create a new client that follows the specified gRPC @@ -112,10 +115,12 @@ final BlockingClient newBlockingClientForCallFactory(GrpcClientCallFactory clien /** * Sets the supported message encodings for this client factory. * By default only {@link Identity#identity()} is supported - * + * @deprecated Use generated code methods targeting {@link List} of + * {@link io.servicetalk.encoding.api.BufferEncoder}s and {@link io.servicetalk.encoding.api.BufferDecoderGroup}. * @param codings The supported encodings {@link ContentCodec}s for this client. * @return {@code this} */ + @Deprecated public GrpcClientFactory supportedMessageCodings(List codings) { this.supportedCodings = unmodifiableList(new ArrayList<>(codings)); @@ -124,12 +129,37 @@ final BlockingClient newBlockingClientForCallFactory(GrpcClientCallFactory clien /** * Return the supported {@link ContentCodec}s for this client factory. + * @deprecated Use generated code methods targeting {@link List} of + * {@link io.servicetalk.encoding.api.BufferEncoder}s and {@link io.servicetalk.encoding.api.BufferDecoderGroup}. * @return the supported {@link ContentCodec}s for this client factory */ + @Deprecated protected List supportedMessageCodings() { return supportedCodings; } + /** + * Sets the supported {@link BufferDecoderGroup} for this client factory. + * By default only {@link Identity#identityEncoder()} is supported + * + * {@link io.servicetalk.encoding.api.BufferDecoderGroup}. + * @param bufferDecoderGroup The supported {@link BufferDecoderGroup} for this client. + * @return {@code this} + */ + public GrpcClientFactory bufferDecoderGroup( + BufferDecoderGroup bufferDecoderGroup) { + this.bufferDecoderGroup = requireNonNull(bufferDecoderGroup); + return this; + } + + /** + * Get the supported {@link BufferDecoderGroup} for this client factory. + * @return the supported {@link BufferDecoderGroup} for this client factory. + */ + protected BufferDecoderGroup bufferDecoderGroup() { + return bufferDecoderGroup; + } + /** * Appends the passed {@link FilterFactory} to this client factory. * diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientMetadata.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientMetadata.java index c9055cde5b..c50859d692 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientMetadata.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcClientMetadata.java @@ -15,6 +15,7 @@ */ package io.servicetalk.grpc.api; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.time.Duration; @@ -38,12 +39,20 @@ public interface GrpcClientMetadata extends GrpcMetadata { /** * {@link ContentCodec} to use for the associated * gRPC method. - * + * @deprecated Use {@link #requestCompressor()}. * @return {@link ContentCodec} to use for the associated * gRPC method. */ + @Deprecated ContentCodec requestEncoding(); + /** + * Get the {@link BufferEncoder} to use to compress the request associated with this object. + * @return the {@link BufferEncoder} to use to compress the request associated with this object. + */ + @Nullable + BufferEncoder requestCompressor(); + /** * Returns timeout duration after which the response is no longer wanted. * diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcDeserializer.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcDeserializer.java new file mode 100644 index 0000000000..8cb39d31fa --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcDeserializer.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2021 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.grpc.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.SerializationException; + +import javax.annotation.Nullable; + +import static io.servicetalk.grpc.api.GrpcStreamingDeserializer.isCompressed; +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.METADATA_SIZE; +import static java.util.Objects.requireNonNull; + +final class GrpcDeserializer implements Deserializer { + private final Deserializer deserializer; + @Nullable + private final BufferDecoder decompressor; + + GrpcDeserializer(final Deserializer deserializer) { + this.deserializer = requireNonNull(deserializer); + this.decompressor = null; + } + + GrpcDeserializer(final Deserializer deserializer, + @Nullable final BufferDecoder decompressor) { + this.deserializer = requireNonNull(deserializer); + this.decompressor = decompressor; + } + + @Nullable + CharSequence messageEncoding() { + return decompressor == null ? null : decompressor.encodingName(); + } + + @Override + public T deserialize(final Buffer buffer, final BufferAllocator allocator) { + if (buffer.readableBytes() < METADATA_SIZE) { + throw new SerializationException("Not enough data"); + } + boolean compressed = isCompressed(buffer); + if (compressed && decompressor == null) { + throw new SerializationException("Compressed flag set, but no compressor"); + } + int expectedLength = buffer.readInt(); + if (expectedLength < 0) { + throw new SerializationException("Message-Length invalid: " + expectedLength); + } + + Buffer result = buffer.readBytes(expectedLength); + if (compressed) { + result = decompressor.decoder().deserialize(result, allocator); + } + return deserializer.deserialize(result, allocator); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcMetadata.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcMetadata.java index ea82eb73e8..db6b424f11 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcMetadata.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcMetadata.java @@ -22,8 +22,9 @@ public interface GrpcMetadata { /** * Returns the path for the associated gRPC method. - * + * @deprecated Use {@link MethodDescriptor#httpPath()}. * @return The path for the associated gRPC method. */ + @Deprecated String path(); } diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRouter.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRouter.java index 5718cfea32..720c2ad4d5 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRouter.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRouter.java @@ -15,6 +15,7 @@ */ package io.servicetalk.grpc.api; +import io.servicetalk.buffer.api.Buffer; import io.servicetalk.concurrent.BlockingIterable; import io.servicetalk.concurrent.BlockingIterator; import io.servicetalk.concurrent.GracefulAutoCloseable; @@ -24,7 +25,9 @@ import io.servicetalk.concurrent.api.CompositeCloseable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.grpc.api.GrpcRoutes.BlockingRequestStreamingRoute; import io.servicetalk.grpc.api.GrpcRoutes.BlockingResponseStreamingRoute; import io.servicetalk.grpc.api.GrpcRoutes.BlockingRoute; @@ -39,19 +42,18 @@ import io.servicetalk.http.api.BlockingStreamingHttpServerResponse; import io.servicetalk.http.api.BlockingStreamingHttpService; import io.servicetalk.http.api.HttpApiConversions.ServiceAdapterHolder; -import io.servicetalk.http.api.HttpDeserializer; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpPayloadWriter; import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; -import io.servicetalk.http.api.HttpSerializer; import io.servicetalk.http.api.HttpService; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; import io.servicetalk.http.api.StreamingHttpResponseFactory; import io.servicetalk.http.api.StreamingHttpService; +import io.servicetalk.oio.api.PayloadWriter; import io.servicetalk.transport.api.ExecutionContext; import io.servicetalk.transport.api.ServerContext; @@ -73,12 +75,16 @@ import static io.servicetalk.grpc.api.GrpcStatus.fromCodeValue; import static io.servicetalk.grpc.api.GrpcStatusCode.INVALID_ARGUMENT; import static io.servicetalk.grpc.api.GrpcStatusCode.UNIMPLEMENTED; -import static io.servicetalk.grpc.api.GrpcUtils.negotiateAcceptedEncoding; +import static io.servicetalk.grpc.api.GrpcUtils.GRPC_CONTENT_TYPE; +import static io.servicetalk.grpc.api.GrpcUtils.grpcContentType; +import static io.servicetalk.grpc.api.GrpcUtils.initResponse; +import static io.servicetalk.grpc.api.GrpcUtils.negotiateAcceptedEncodingRaw; import static io.servicetalk.grpc.api.GrpcUtils.newErrorResponse; import static io.servicetalk.grpc.api.GrpcUtils.newResponse; -import static io.servicetalk.grpc.api.GrpcUtils.readGrpcMessageEncoding; +import static io.servicetalk.grpc.api.GrpcUtils.readGrpcMessageEncodingRaw; import static io.servicetalk.grpc.api.GrpcUtils.setStatus; import static io.servicetalk.grpc.api.GrpcUtils.setStatusOk; +import static io.servicetalk.grpc.api.GrpcUtils.validateContentType; import static io.servicetalk.http.api.HttpApiConversions.toStreamingHttpService; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpRequestMethod.POST; @@ -97,8 +103,8 @@ final class GrpcRouter { private static final GrpcStatus STATUS_UNIMPLEMENTED = fromCodeValue(UNIMPLEMENTED.value()); private static final StreamingHttpService NOT_FOUND_SERVICE = (ctx, request, responseFactory) -> { - final StreamingHttpResponse response = newErrorResponse(responseFactory, null, STATUS_UNIMPLEMENTED, - null, ctx.executionContext().bufferAllocator()); + final StreamingHttpResponse response = newErrorResponse(responseFactory, GRPC_CONTENT_TYPE, + STATUS_UNIMPLEMENTED.asException(), ctx.executionContext().bufferAllocator()); response.version(request.version()); return succeeded(response); }; @@ -236,38 +242,45 @@ private static void mergeRoutes(final Map first, } } - Builder addRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final Route route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - verifyNoOverrides(routes.put(path, new RouteProvider(executionContext -> toStreamingHttpService( + void addRoute(MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, Route route) { + GrpcSerializer serializerIdentity = serializer(methodDescriptor); + List> serializers = serializers(methodDescriptor, compressors); + GrpcDeserializer deserializerIdentity = deserializer(methodDescriptor); + List> deserializers = deserializers(methodDescriptor, decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); + verifyNoOverrides(routes.put(methodDescriptor.httpPath(), + new RouteProvider(executionContext -> toStreamingHttpService( new HttpService() { - @Override public Single handle(final HttpServiceContext ctx, final HttpRequest request, final HttpResponseFactory responseFactory) { - - ContentCodec responseEncoding; - GrpcServiceContext serviceContext = null; try { - final List supportedCodings = - serializationProvider.supportedMessageCodings(); - responseEncoding = negotiateAcceptedEncoding(request, supportedCodings); - serviceContext = new DefaultGrpcServiceContext(request.path(), ctx, supportedCodings); - final HttpDeserializer deserializer = - serializationProvider.deserializerFor( - readGrpcMessageEncoding(request, supportedCodings), requestClass); - final GrpcServiceContext finalServiceContext = serviceContext; - return route.handle(serviceContext, request.payloadBody(deserializer)) - .map(rawResp -> newResponse(responseFactory, finalServiceContext, - ctx.executionContext().bufferAllocator()) - .payloadBody(rawResp, - serializationProvider.serializerFor(responseEncoding, - responseClass))) - .onErrorReturn(cause -> newErrorResponse(responseFactory, finalServiceContext, - null, cause, ctx.executionContext().bufferAllocator())); + validateContentType(request.headers(), requestContentType); + GrpcDeserializer deserializer = readGrpcMessageEncodingRaw( + request.headers(), deserializerIdentity, deserializers, + GrpcDeserializer::messageEncoding); + return route.handle(new DefaultGrpcServiceContext(methodDescriptor.httpPath(), ctx), + deserializer.deserialize(request.payloadBody(), + ctx.executionContext().bufferAllocator())) + .map(rawResp -> { + GrpcSerializer serializer = negotiateAcceptedEncodingRaw( + request.headers(), serializerIdentity, serializers, + GrpcSerializer::messageEncoding); + return newResponse(responseFactory, responseContentType, + serializer.messageEncoding(), acceptedEncoding) + .payloadBody(serializer.serialize(rawResp, + ctx.executionContext().bufferAllocator())); + }) + .onErrorReturn(cause -> newErrorResponse(responseFactory, + responseContentType, cause, ctx.executionContext().bufferAllocator())); } catch (Throwable t) { - return succeeded(newErrorResponse(responseFactory, serviceContext, null, t, + return succeeded(newErrorResponse(responseFactory, responseContentType, t, ctx.executionContext().bufferAllocator())); } } @@ -287,80 +300,86 @@ public Completable closeAsyncGracefully() { // We only assume duplication across blocking and async variant of the same API and not between // aggregated and streaming. Therefore, verify that there is no blocking-aggregated route registered // for the same path: - path, blockingRoutes); - executionStrategies.put(path, executionStrategy); - return this; - } - - Builder addStreamingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final StreamingRoute route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - verifyNoOverrides(streamingRoutes.put(path, new RouteProvider(executionContext -> { - final StreamingHttpService service = new StreamingHttpService() { - - @Override - public Single handle(final HttpServiceContext ctx, - final StreamingHttpRequest request, - final StreamingHttpResponseFactory responseFactory) { - ContentCodec responseEncoding; - GrpcServiceContext serviceContext = null; - - try { - final List supportedCodings = - serializationProvider.supportedMessageCodings(); - responseEncoding = negotiateAcceptedEncoding(request, supportedCodings); - serviceContext = new DefaultGrpcServiceContext(request.path(), ctx, supportedCodings); - final HttpDeserializer deserializer = - serializationProvider.deserializerFor( - readGrpcMessageEncoding(request, supportedCodings), requestClass); - final Publisher response = route.handle(serviceContext, - request.payloadBody(deserializer)); - return succeeded(newResponse(responseFactory, serviceContext, response, - serializationProvider.serializerFor(responseEncoding, responseClass), - ctx.executionContext().bufferAllocator())); - } catch (Throwable t) { - return succeeded(newErrorResponse(responseFactory, serviceContext, null, t, - ctx.executionContext().bufferAllocator())); - } - } - - @Override - public Completable closeAsync() { - return route.closeAsync(); - } - - @Override - public Completable closeAsyncGracefully() { - return route.closeAsyncGracefully(); - } - }; - return new ServiceAdapterHolder() { - @Override - public StreamingHttpService adaptor() { - return service; - } - - @Override - public HttpExecutionStrategy serviceInvocationStrategy() { - return executionStrategy == null ? defaultStrategy() : executionStrategy; - } - }; - }, () -> route, () -> toRequestStreamingRoute(route), () -> toResponseStreamingRoute(route), - () -> toRoute(route), route)), + methodDescriptor.httpPath(), blockingRoutes); + executionStrategies.put(methodDescriptor.httpPath(), executionStrategy); + } + + void addStreamingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, StreamingRoute route) { + GrpcStreamingSerializer serializerIdentity = streamingSerializer(methodDescriptor); + List> serializers = streamingSerializers(methodDescriptor, compressors); + GrpcStreamingDeserializer deserializerIdentity = streamingDeserializer(methodDescriptor); + List> deserializers = + streamingDeserializers(methodDescriptor, decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); + verifyNoOverrides(streamingRoutes.put(methodDescriptor.httpPath(), new RouteProvider(executionContext -> { + final StreamingHttpService service = new StreamingHttpService() { + @Override + public Single handle( + final HttpServiceContext ctx, final StreamingHttpRequest request, + final StreamingHttpResponseFactory responseFactory) { + try { + validateContentType(request.headers(), requestContentType); + GrpcStreamingSerializer serializer = negotiateAcceptedEncodingRaw( + request.headers(), serializerIdentity, serializers, + GrpcStreamingSerializer::messageEncoding); + GrpcStreamingDeserializer deserializer = readGrpcMessageEncodingRaw( + request.headers(), deserializerIdentity, deserializers, + GrpcStreamingDeserializer::messageEncoding); + final Publisher response = route.handle( + new DefaultGrpcServiceContext(methodDescriptor.httpPath(), ctx), + deserializer.deserialize(request.payloadBody(), + ctx.executionContext().bufferAllocator())); + return succeeded(newResponse(responseFactory, responseContentType, + serializer.messageEncoding(), acceptedEncoding, response, serializer, + ctx.executionContext().bufferAllocator())); + } catch (Throwable t) { + return succeeded(newErrorResponse(responseFactory, responseContentType, t, + ctx.executionContext().bufferAllocator())); + } + } + + @Override + public Completable closeAsync() { + return route.closeAsync(); + } + + @Override + public Completable closeAsyncGracefully() { + return route.closeAsyncGracefully(); + } + }; + return new ServiceAdapterHolder() { + @Override + public StreamingHttpService adaptor() { + return service; + } + + @Override + public HttpExecutionStrategy serviceInvocationStrategy() { + return executionStrategy == null ? defaultStrategy() : executionStrategy; + } + }; + }, () -> route, () -> toRequestStreamingRoute(route), () -> toResponseStreamingRoute(route), + () -> toRoute(route), route)), // We only assume duplication across blocking and async variant of the same API and not between // aggregated and streaming. Therefore, verify that there is no blocking-streaming route registered // for the same path: - path, blockingStreamingRoutes); - executionStrategies.put(path, executionStrategy); - return this; + methodDescriptor.httpPath(), blockingStreamingRoutes); + executionStrategies.put(methodDescriptor.httpPath(), executionStrategy); } - Builder addRequestStreamingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final RequestStreamingRoute route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - return addStreamingRoute(path, executionStrategy, + void addRequestStreamingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, RequestStreamingRoute route) { + addStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, new StreamingRoute() { @Override @@ -377,27 +396,15 @@ public Completable closeAsync() { public Completable closeAsyncGracefully() { return route.closeAsyncGracefully(); } - }, requestClass, responseClass, serializationProvider); + }); } - /** - * Adds a {@link ResponseStreamingRoute} to this builder. - * - * @param path for this route. - * @param route {@link ResponseStreamingRoute} to add. - * @param requestClass {@link Class} for the request object. - * @param responseClass {@link Class} for the response object. - * @param serializationProvider {@link GrpcSerializationProvider} for the route. - * @param Type of request. - * @param Type of response. - * @return {@code this}. - */ - Builder addResponseStreamingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final ResponseStreamingRoute route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - return addStreamingRoute(path, executionStrategy, new StreamingRoute() { - + void addResponseStreamingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, ResponseStreamingRoute route) { + addStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, + new StreamingRoute() { @Override public Publisher handle(final GrpcServiceContext ctx, final Publisher request) { return request.firstOrError() @@ -425,47 +432,45 @@ public Completable closeAsync() { public Completable closeAsyncGracefully() { return route.closeAsyncGracefully(); } - }, requestClass, responseClass, serializationProvider); - } - - /** - * Adds a {@link BlockingRoute} to this builder. - * - * @param path for this route. - * @param route {@link BlockingRoute} to add. - * @param requestClass {@link Class} for the request object. - * @param responseClass {@link Class} for the response object. - * @param serializationProvider {@link GrpcSerializationProvider} for the route. - * @param Type of request. - * @param Type of response. - * @return {@code this}. - */ - Builder addBlockingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final BlockingRoute route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - verifyNoOverrides(blockingRoutes.put(path, new RouteProvider(executionContext -> + }); + } + + void addBlockingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, BlockingRoute route) { + GrpcSerializer serializerIdentity = serializer(methodDescriptor); + List> serializers = serializers(methodDescriptor, compressors); + GrpcDeserializer deserializerIdentity = deserializer(methodDescriptor); + List> deserializers = deserializers(methodDescriptor, decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); + verifyNoOverrides(blockingRoutes.put(methodDescriptor.httpPath(), new RouteProvider(executionContext -> toStreamingHttpService(new BlockingHttpService() { - @Override public HttpResponse handle(final HttpServiceContext ctx, final HttpRequest request, final HttpResponseFactory responseFactory) { - ContentCodec responseEncoding; - GrpcServiceContext serviceContext = null; try { - final List supportedCodings = - serializationProvider.supportedMessageCodings(); - responseEncoding = negotiateAcceptedEncoding(request, supportedCodings); - serviceContext = new DefaultGrpcServiceContext(request.path(), ctx, supportedCodings); - final HttpDeserializer deserializer = - serializationProvider.deserializerFor( - readGrpcMessageEncoding(request, supportedCodings), requestClass); - final Resp response = route.handle(serviceContext, request.payloadBody(deserializer)); - return newResponse(responseFactory, serviceContext, - ctx.executionContext().bufferAllocator()).payloadBody(response, - serializationProvider.serializerFor(responseEncoding, responseClass)); + validateContentType(request.headers(), requestContentType); + GrpcDeserializer deserializer = readGrpcMessageEncodingRaw( + request.headers(), deserializerIdentity, deserializers, + GrpcDeserializer::messageEncoding); + final Resp rawResp = route.handle( + new DefaultGrpcServiceContext(methodDescriptor.httpPath(), ctx), + deserializer.deserialize(request.payloadBody(), + ctx.executionContext().bufferAllocator())); + GrpcSerializer serializer = negotiateAcceptedEncodingRaw( + request.headers(), serializerIdentity, serializers, + GrpcSerializer::messageEncoding); + return newResponse(responseFactory, responseContentType, + serializer.messageEncoding(), acceptedEncoding) + .payloadBody(serializer.serialize(rawResp, + ctx.executionContext().bufferAllocator())); } catch (Throwable t) { - return newErrorResponse(responseFactory, serviceContext, null, t, + return newErrorResponse(responseFactory, responseContentType, t, ctx.executionContext().bufferAllocator()); } } @@ -485,53 +490,51 @@ public void closeGracefully() throws Exception { // We only assume duplication across blocking and async variant of the same API and not between // aggregated and streaming. Therefore, verify that there is no async-aggregated route registered // for the same path: - path, routes); - executionStrategies.put(path, executionStrategy); - return this; - } - - /** - * Adds a {@link BlockingStreamingRoute} to this builder. - * - * @param path for this route. - * @param route {@link BlockingStreamingRoute} to add. - * @param requestClass {@link Class} for the request object. - * @param responseClass {@link Class} for the response object. - * @param serializationProvider {@link GrpcSerializationProvider} for the route. - * @param Type of request. - * @param Type of response. - * @return {@code this}. - */ - Builder addBlockingStreamingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final BlockingStreamingRoute route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - verifyNoOverrides(blockingStreamingRoutes.put(path, new RouteProvider(executionContext -> - toStreamingHttpService(new BlockingStreamingHttpService() { + methodDescriptor.httpPath(), routes); + executionStrategies.put(methodDescriptor.httpPath(), executionStrategy); + } + + void addBlockingStreamingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, BlockingStreamingRoute route) { + GrpcStreamingSerializer serializerIdentity = streamingSerializer(methodDescriptor); + List> serializers = streamingSerializers(methodDescriptor, compressors); + GrpcStreamingDeserializer deserializerIdentity = streamingDeserializer(methodDescriptor); + List> deserializers = + streamingDeserializers(methodDescriptor, decompressors.decoders()); + CharSequence acceptedEncoding = decompressors.advertisedMessageEncoding(); + CharSequence requestContentType = grpcContentType(methodDescriptor.requestDescriptor() + .serializerDescriptor().contentType()); + CharSequence responseContentType = grpcContentType(methodDescriptor.responseDescriptor() + .serializerDescriptor().contentType()); + verifyNoOverrides(blockingStreamingRoutes.put(methodDescriptor.httpPath(), + new RouteProvider(executionContext -> toStreamingHttpService(new BlockingStreamingHttpService() { @Override public void handle(final HttpServiceContext ctx, final BlockingStreamingHttpRequest request, final BlockingStreamingHttpServerResponse response) throws Exception { - final List supportedCodings = - serializationProvider.supportedMessageCodings(); - final ContentCodec responseEncoding = negotiateAcceptedEncoding(request, - supportedCodings); + validateContentType(request.headers(), requestContentType); + GrpcStreamingSerializer serializer = negotiateAcceptedEncodingRaw( + request.headers(), serializerIdentity, serializers, + GrpcStreamingSerializer::messageEncoding); + GrpcStreamingDeserializer deserializer = readGrpcMessageEncodingRaw( + request.headers(), deserializerIdentity, deserializers, + GrpcStreamingDeserializer::messageEncoding); final GrpcServiceContext serviceContext = - new DefaultGrpcServiceContext(request.path(), ctx, supportedCodings); - final HttpDeserializer deserializer = serializationProvider.deserializerFor( - readGrpcMessageEncoding(request, supportedCodings), requestClass); - final HttpSerializer serializer = - serializationProvider.serializerFor(responseEncoding, responseClass); + new DefaultGrpcServiceContext(request.path(), ctx); + initResponse(response, responseContentType, serializer.messageEncoding(), acceptedEncoding); + final HttpPayloadWriter bufferWriter = response.sendMetaData(); final DefaultGrpcPayloadWriter grpcPayloadWriter = - new DefaultGrpcPayloadWriter<>(response.sendMetaData(serializer)); + new DefaultGrpcPayloadWriter<>(serializer.serialize(bufferWriter, + ctx.executionContext().bufferAllocator())); try { // Set status OK before invoking handle methods because users can close PayloadWriter - final HttpPayloadWriter payloadWriter = grpcPayloadWriter.payloadWriter(); - setStatusOk(payloadWriter.trailers(), ctx.executionContext().bufferAllocator()); - route.handle(serviceContext, request.payloadBody(deserializer), grpcPayloadWriter); + setStatusOk(bufferWriter.trailers()); + route.handle(serviceContext, deserializer.deserialize(request.payloadBody(), + ctx.executionContext().bufferAllocator()), grpcPayloadWriter); } catch (Throwable t) { try { - final HttpPayloadWriter payloadWriter = grpcPayloadWriter.payloadWriter(); - setStatus(payloadWriter.trailers(), t, ctx.executionContext().bufferAllocator()); + setStatus(bufferWriter.trailers(), t, ctx.executionContext().bufferAllocator()); } finally { // Error is propagated in trailers, payload should close normally. grpcPayloadWriter.close(); @@ -554,29 +557,15 @@ public void closeGracefully() throws Exception { // We only assume duplication across blocking and async variant of the same API and not between // aggregated and streaming. Therefore, verify that there is no async-streaming route registered // for the same path: - path, streamingRoutes); - executionStrategies.put(path, executionStrategy); - return this; + methodDescriptor.httpPath(), streamingRoutes); + executionStrategies.put(methodDescriptor.httpPath(), executionStrategy); } - /** - * Adds a {@link RequestStreamingRoute} to this builder. - * - * @param path for this route. - * @param route {@link RequestStreamingRoute} to add. - * @param requestClass {@link Class} for the request object. - * @param responseClass {@link Class} for the response object. - * @param serializationProvider {@link GrpcSerializationProvider} for the route. - * @param Type of request. - * @param Type of response. - * @return {@code this}. - */ - Builder addBlockingRequestStreamingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final BlockingRequestStreamingRoute route, - final Class requestClass, final Class responseClass, - final GrpcSerializationProvider serializationProvider) { - return addBlockingStreamingRoute(path, executionStrategy, + void addBlockingRequestStreamingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, BlockingRequestStreamingRoute route) { + addBlockingStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, new BlockingStreamingRoute() { @Override public void handle(final GrpcServiceContext ctx, final BlockingIterable request, @@ -595,29 +584,16 @@ public void close() throws Exception { public void closeGracefully() throws Exception { route.closeGracefully(); } - }, - requestClass, responseClass, serializationProvider); + }); } - /** - * Adds a {@link ResponseStreamingRoute} to this builder. - * - * @param path for this route. - * @param route {@link ResponseStreamingRoute} to add. - * @param requestClass {@link Class} for the request object. - * @param responseClass {@link Class} for the response object. - * @param serializationProvider {@link GrpcSerializationProvider} for the route. - * @param Type of request. - * @param Type of response. - * @return {@code this}. - */ - Builder addBlockingResponseStreamingRoute( - final String path, @Nullable final GrpcExecutionStrategy executionStrategy, - final BlockingResponseStreamingRoute route, final Class requestClass, - final Class responseClass, final GrpcSerializationProvider serializationProvider) { - return addBlockingStreamingRoute(path, executionStrategy, + void addBlockingResponseStreamingRoute( + MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + @Nullable GrpcExecutionStrategy executionStrategy, + final BlockingResponseStreamingRoute route) { + addBlockingStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, new BlockingStreamingRoute() { - @Override public void handle(final GrpcServiceContext ctx, final BlockingIterable request, final GrpcPayloadWriter responseWriter) throws Exception { @@ -648,8 +624,7 @@ public void close() throws Exception { public void closeGracefully() throws Exception { route.closeGracefully(); } - }, - requestClass, responseClass, serializationProvider); + }); } /** @@ -669,10 +644,56 @@ private static void verifyNoOverrides(@Nullable final Object oldValue, final Str } } + private static List> streamingDeserializers( + MethodDescriptor methodDescriptor, List decompressors) { + return GrpcUtils.streamingDeserializers( + methodDescriptor.requestDescriptor().serializerDescriptor().serializer(), decompressors); + } + + private static List> streamingSerializers( + MethodDescriptor methodDescriptor, List compressors) { + return GrpcUtils.streamingSerializers(methodDescriptor.responseDescriptor().serializerDescriptor().serializer(), + methodDescriptor.responseDescriptor().serializerDescriptor().bytesEstimator(), compressors); + } + + private static GrpcStreamingSerializer streamingSerializer( + MethodDescriptor methodDescriptor) { + return new GrpcStreamingSerializer<>( + methodDescriptor.responseDescriptor().serializerDescriptor().bytesEstimator(), + methodDescriptor.responseDescriptor().serializerDescriptor().serializer()); + } + + private static GrpcStreamingDeserializer streamingDeserializer( + MethodDescriptor methodDescriptor) { + return new GrpcStreamingDeserializer<>( + methodDescriptor.requestDescriptor().serializerDescriptor().serializer()); + } + + private static List> deserializers( + MethodDescriptor methodDescriptor, List decompressors) { + return GrpcUtils.deserializers(methodDescriptor.requestDescriptor().serializerDescriptor().serializer(), + decompressors); + } + + private static List> serializers( + MethodDescriptor methodDescriptor, List compressors) { + return GrpcUtils.serializers(methodDescriptor.responseDescriptor().serializerDescriptor().serializer(), + methodDescriptor.responseDescriptor().serializerDescriptor().bytesEstimator(), compressors); + } + + private static GrpcSerializer serializer(MethodDescriptor methodDescriptor) { + return new GrpcSerializer<>(methodDescriptor.responseDescriptor().serializerDescriptor().bytesEstimator(), + methodDescriptor.responseDescriptor().serializerDescriptor().serializer()); + } + + private static GrpcDeserializer deserializer(MethodDescriptor methodDescriptor) { + return new GrpcDeserializer<>(methodDescriptor.requestDescriptor().serializerDescriptor().serializer()); + } + private static final class DefaultGrpcPayloadWriter implements GrpcPayloadWriter { - private final HttpPayloadWriter payloadWriter; + private final PayloadWriter payloadWriter; - DefaultGrpcPayloadWriter(final HttpPayloadWriter payloadWriter) { + DefaultGrpcPayloadWriter(final PayloadWriter payloadWriter) { this.payloadWriter = payloadWriter; } @@ -695,10 +716,6 @@ public void close(final Throwable cause) throws IOException { public void flush() throws IOException { payloadWriter.flush(); } - - HttpPayloadWriter payloadWriter() { - return payloadWriter; - } } static final class RouteProviders implements AsyncCloseable { diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRoutes.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRoutes.java index 7aefb84005..95e94370f1 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRoutes.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcRoutes.java @@ -21,21 +21,31 @@ import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.grpc.api.GrpcRouter.RouteProviders; import io.servicetalk.grpc.api.GrpcServiceFactory.ServerBinder; +import io.servicetalk.grpc.api.GrpcUtils.DefaultMethodDescriptor; import io.servicetalk.http.api.HttpExecutionStrategies; +import io.servicetalk.router.api.NoOffloadsRouteExecutionStrategy; import io.servicetalk.router.api.RouteExecutionStrategy; import io.servicetalk.router.api.RouteExecutionStrategyFactory; import io.servicetalk.transport.api.ExecutionContext; import io.servicetalk.transport.api.ServerContext; import java.lang.reflect.Method; +import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.annotation.Nullable; import static io.servicetalk.concurrent.api.Completable.completed; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.noOffloadsStrategy; +import static io.servicetalk.grpc.api.GrpcUtils.GRPC_PROTO_CONTENT_TYPE; +import static io.servicetalk.grpc.api.GrpcUtils.compressors; +import static io.servicetalk.grpc.api.GrpcUtils.decompressors; +import static io.servicetalk.grpc.api.GrpcUtils.defaultToInt; +import static io.servicetalk.grpc.api.GrpcUtils.serializerDeserializer; import static io.servicetalk.router.utils.internal.DefaultRouteExecutionStrategyFactory.defaultStrategyFactory; import static io.servicetalk.router.utils.internal.RouteExecutionStrategyUtils.getAndValidateRouteExecutionStrategyAnnotationIfPresent; import static io.servicetalk.utils.internal.ReflectionUtils.retrieveMethod; @@ -46,7 +56,6 @@ * @param Type for service that these routes represent. */ public abstract class GrpcRoutes { - private static final GrpcExecutionStrategy NULL = new DefaultGrpcExecutionStrategy( HttpExecutionStrategies.noOffloadsStrategy()); @@ -193,7 +202,7 @@ private GrpcExecutionStrategy executionStrategy(final String path, final Method /** * Adds a {@link Route} to this factory. - * + * @deprecated Use {@link #addRoute(Class, MethodDescriptor, BufferDecoderGroup, List, Route)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -204,18 +213,43 @@ private GrpcExecutionStrategy executionStrategy(final String path, final Method * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addRoute( final String path, final Class serviceClass, final String methodName, final Route route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, requestClass); - routeBuilder.addRoute(path, executionStrategy(path, method, serviceClass), route, - requestClass, responseClass, serializationProvider); + addRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link Route} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to each response. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, Route route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + methodDescriptor.requestDescriptor().parameterClass()); + routeBuilder.addRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link Route} to this factory. + * @deprecated Use {@link #addRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, Route)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link Route} to add. @@ -225,17 +259,39 @@ protected final void addRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addRoute( final String path, final GrpcExecutionStrategy executionStrategy, final Route route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addRoute(path, executionStrategy, route, requestClass, responseClass, - serializationProvider); + addRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link Route} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to each response. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, Route route) { + routeBuilder.addRoute(methodDescriptor, decompressors, compressors, executionStrategy, route); } /** * Adds a {@link StreamingRoute} to this factory. - * + * @deprecated Use {@link #addStreamingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, StreamingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -246,18 +302,44 @@ protected final void addRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addStreamingRoute( final String path, final Class serviceClass, final String methodName, final StreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, Publisher.class); - routeBuilder.addStreamingRoute(path, executionStrategy(path, method, serviceClass), route, - requestClass, responseClass, serializationProvider); + addStreamingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + true, true, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link StreamingRoute} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addStreamingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, StreamingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + Publisher.class); + routeBuilder.addStreamingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link StreamingRoute} to this factory. + * @deprecated Use {@link #addStreamingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * StreamingRoute)} * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link StreamingRoute} to add. @@ -267,17 +349,40 @@ protected final void addStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addStreamingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final StreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addStreamingRoute(path, executionStrategy, route, requestClass, responseClass, - serializationProvider); + addStreamingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + true, true, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link StreamingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addStreamingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, StreamingRoute route) { + routeBuilder.addStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, route); } /** * Adds a {@link RequestStreamingRoute} to this factory. - * + * @deprecated Use + * {@link #addRequestStreamingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, RequestStreamingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -288,18 +393,46 @@ protected final void addStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addRequestStreamingRoute( final String path, final Class serviceClass, final String methodName, final RequestStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, Publisher.class); - routeBuilder.addRequestStreamingRoute(path, executionStrategy(path, method, serviceClass), - route, requestClass, responseClass, serializationProvider); + addRequestStreamingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + true, true, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link RequestStreamingRoute} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addRequestStreamingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + RequestStreamingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + Publisher.class); + routeBuilder.addRequestStreamingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link RequestStreamingRoute} to this factory. + * @deprecated Use + * {@link #addRequestStreamingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * RequestStreamingRoute)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link RequestStreamingRoute} to add. @@ -309,17 +442,41 @@ protected final void addRequestStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addRequestStreamingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final RequestStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addRequestStreamingRoute(path, executionStrategy, route, requestClass, - responseClass, serializationProvider); + addRequestStreamingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + true, true, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link RequestStreamingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addRequestStreamingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + RequestStreamingRoute route) { + routeBuilder.addRequestStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, route); } /** * Adds a {@link ResponseStreamingRoute} to this factory. - * + * @deprecated Use + * {@link #addResponseStreamingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, ResponseStreamingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -330,18 +487,46 @@ protected final void addRequestStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addResponseStreamingRoute( final String path, final Class serviceClass, final String methodName, final ResponseStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, requestClass); - routeBuilder.addResponseStreamingRoute(path, executionStrategy(path, method, serviceClass), - route, requestClass, responseClass, serializationProvider); + addResponseStreamingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link ResponseStreamingRoute} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addResponseStreamingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + ResponseStreamingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + methodDescriptor.requestDescriptor().parameterClass()); + routeBuilder.addResponseStreamingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link ResponseStreamingRoute} to this factory. + * @deprecated Use + * {@link #addResponseStreamingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * ResponseStreamingRoute)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link ResponseStreamingRoute} to add. @@ -351,17 +536,40 @@ protected final void addResponseStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addResponseStreamingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final ResponseStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addResponseStreamingRoute(path, executionStrategy, route, requestClass, - responseClass, serializationProvider); + addResponseStreamingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, true, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link ResponseStreamingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addResponseStreamingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + ResponseStreamingRoute route) { + routeBuilder.addResponseStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, route); } /** * Adds a {@link BlockingRoute} to this factory. - * + * @deprecated Use {@link #addBlockingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, BlockingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -372,18 +580,45 @@ protected final void addResponseStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingRoute( final String path, final Class serviceClass, final String methodName, final BlockingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, requestClass); - routeBuilder.addBlockingRoute(path, executionStrategy(path, method, serviceClass), route, - requestClass, responseClass, serializationProvider); + addBlockingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link BlockingRoute} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + methodDescriptor.requestDescriptor().parameterClass()); + routeBuilder.addBlockingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link BlockingRoute} to this factory. + * @deprecated Use {@link #addBlockingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * BlockingRoute)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link BlockingRoute} to add. @@ -393,17 +628,41 @@ protected final void addBlockingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final BlockingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addBlockingRoute(path, executionStrategy, route, requestClass, responseClass, - serializationProvider); + addBlockingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link BlockingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingRoute route) { + routeBuilder.addBlockingRoute(methodDescriptor, decompressors, compressors, executionStrategy, route); } /** * Adds a {@link BlockingStreamingRoute} to this factory. - * + * @deprecated Use + * {@link #addBlockingStreamingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, BlockingStreamingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -414,19 +673,46 @@ protected final void addBlockingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingStreamingRoute( final String path, final Class serviceClass, final String methodName, final BlockingStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, BlockingIterable.class, - GrpcPayloadWriter.class); - routeBuilder.addBlockingStreamingRoute(path, executionStrategy(path, method, serviceClass), - route, requestClass, responseClass, serializationProvider); + addBlockingStreamingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + true, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link BlockingStreamingRoute} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingStreamingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingStreamingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + BlockingIterable.class, GrpcPayloadWriter.class); + routeBuilder.addBlockingStreamingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link BlockingStreamingRoute} to this factory. + * @deprecated Use + * {@link #addBlockingStreamingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * BlockingStreamingRoute)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link BlockingStreamingRoute} to add. @@ -436,17 +722,41 @@ protected final void addBlockingStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingStreamingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final BlockingStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addBlockingStreamingRoute(path, executionStrategy, route, requestClass, - responseClass, serializationProvider); + addBlockingStreamingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + true, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link BlockingStreamingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingStreamingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingStreamingRoute route) { + routeBuilder.addBlockingStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, route); } /** * Adds a {@link BlockingRequestStreamingRoute} to this factory. - * + * @deprecated Use + * {@link #addBlockingStreamingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, BlockingStreamingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -457,19 +767,46 @@ protected final void addBlockingStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingRequestStreamingRoute( final String path, final Class serviceClass, final String methodName, final BlockingRequestStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, + addBlockingRequestStreamingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + true, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link BlockingRequestStreamingRoute} to this factory. + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingRequestStreamingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingRequestStreamingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, BlockingIterable.class); - routeBuilder.addBlockingRequestStreamingRoute(path, executionStrategy(path, method, serviceClass), - route, requestClass, responseClass, serializationProvider); + routeBuilder.addBlockingRequestStreamingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); } /** * Adds a {@link BlockingRequestStreamingRoute} to this factory. - * + * @deprecated Use + * {@link #addBlockingRequestStreamingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * BlockingRequestStreamingRoute)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link BlockingRequestStreamingRoute} to add. @@ -479,17 +816,43 @@ protected final void addBlockingRequestStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingRequestStreamingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final BlockingRequestStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addBlockingRequestStreamingRoute(path, executionStrategy, route, requestClass, - responseClass, serializationProvider); + addBlockingRequestStreamingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + true, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + false, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link BlockingRequestStreamingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingRequestStreamingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingRequestStreamingRoute route) { + routeBuilder.addBlockingRequestStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, + route); } /** * Adds a {@link BlockingResponseStreamingRoute} to this factory. - * + * @deprecated Use + * {@link #addBlockingResponseStreamingRoute(Class, MethodDescriptor, BufferDecoderGroup, List, + * BlockingResponseStreamingRoute)}. * @param path for this route. * @param serviceClass {@link Class} of the gRPC service. * @param methodName the name of gRPC method. @@ -500,19 +863,46 @@ protected final void addBlockingRequestStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingResponseStreamingRoute( final String path, final Class serviceClass, final String methodName, final BlockingResponseStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - final Method method = retrieveMethod(serviceClass, methodName, GrpcServiceContext.class, requestClass, - GrpcPayloadWriter.class); - routeBuilder.addBlockingResponseStreamingRoute(path, executionStrategy(path, method, serviceClass), - route, requestClass, responseClass, serializationProvider); + addBlockingResponseStreamingRoute(serviceClass, new DefaultMethodDescriptor<>(path, methodName, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); } /** * Adds a {@link BlockingResponseStreamingRoute} to this factory. - * + * @param serviceClass {@link Class} of the gRPC service which can be used to extract annotations to override + * offloading behavior (e.g. {@link NoOffloadsRouteExecutionStrategy}, {@link RouteExecutionStrategy}). + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingResponseStreamingRoute( + Class serviceClass, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingResponseStreamingRoute route) { + final Method method = retrieveMethod(serviceClass, methodDescriptor.javaMethodName(), GrpcServiceContext.class, + methodDescriptor.requestDescriptor().parameterClass(), GrpcPayloadWriter.class); + routeBuilder.addBlockingResponseStreamingRoute(methodDescriptor, decompressors, compressors, + executionStrategy(methodDescriptor.httpPath(), method, serviceClass), route); + } + + /** + * Adds a {@link BlockingResponseStreamingRoute} to this factory. + * @deprecated Use + * {@link #addBlockingResponseStreamingRoute(GrpcExecutionStrategy, MethodDescriptor, BufferDecoderGroup, List, + * BlockingResponseStreamingRoute)}. * @param path for this route. * @param executionStrategy {@link GrpcExecutionStrategy} to use. * @param route {@link BlockingResponseStreamingRoute} to add. @@ -522,12 +912,36 @@ protected final void addBlockingResponseStreamingRoute( * @param Type of request. * @param Type of response. */ + @Deprecated protected final void addBlockingResponseStreamingRoute( final String path, final GrpcExecutionStrategy executionStrategy, final BlockingResponseStreamingRoute route, final Class requestClass, final Class responseClass, final GrpcSerializationProvider serializationProvider) { - routeBuilder.addBlockingResponseStreamingRoute(path, executionStrategy, route, requestClass, - responseClass, serializationProvider); + addBlockingResponseStreamingRoute(executionStrategy, new DefaultMethodDescriptor<>(path, + false, false, requestClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, requestClass), defaultToInt(), + true, false, responseClass, GRPC_PROTO_CONTENT_TYPE, + serializerDeserializer(serializationProvider, responseClass), defaultToInt()), + decompressors(serializationProvider.supportedMessageCodings()), + compressors(serializationProvider.supportedMessageCodings()), route); + } + + /** + * Adds a {@link BlockingResponseStreamingRoute} to this factory. + * @param executionStrategy The execution strategy to use for this route. + * @param methodDescriptor Describes the method routing and serialization. + * @param decompressors Indicates the supported decompression applied to each request. + * @param compressors Indicates the supported compression can be applied to responses. + * @param route The interface to invoke when data is received for this route. + * @param Type of request. + * @param Type of response. + */ + protected final void addBlockingResponseStreamingRoute( + final GrpcExecutionStrategy executionStrategy, MethodDescriptor methodDescriptor, + BufferDecoderGroup decompressors, List compressors, + BlockingResponseStreamingRoute route) { + routeBuilder.addBlockingResponseStreamingRoute(methodDescriptor, decompressors, compressors, executionStrategy, + route); } /** diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializationProvider.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializationProvider.java index 0200e04414..cd960f55fd 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializationProvider.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializationProvider.java @@ -23,7 +23,10 @@ /** * A provider for gRPC serialization/deserialization. + * @deprecated Serialization is now specified via {@link MethodDescriptor}. Compression is configured per route. + * gRPC framing is internalized in the gRPC implementation. */ +@Deprecated public interface GrpcSerializationProvider { /** diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializer.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializer.java new file mode 100644 index 0000000000..1bf5c3705b --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcSerializer.java @@ -0,0 +1,87 @@ +/* + * Copyright © 2021 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.grpc.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.serializer.api.Serializer; + +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.FLAG_COMPRESSED; +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.FLAG_UNCOMPRESSED; +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.METADATA_SIZE; +import static java.util.Objects.requireNonNull; + +final class GrpcSerializer implements Serializer { + private final ToIntFunction serializedBytesEstimator; + private final Serializer serializer; + @Nullable + private final BufferEncoder compressor; + + GrpcSerializer(final ToIntFunction serializedBytesEstimator, + final Serializer serializer) { + this.serializedBytesEstimator = requireNonNull(serializedBytesEstimator); + this.serializer = requireNonNull(serializer); + this.compressor = null; + } + + GrpcSerializer(final ToIntFunction serializedBytesEstimator, + final Serializer serializer, + final BufferEncoder compressor) { + this.serializedBytesEstimator = requireNonNull(serializedBytesEstimator); + this.serializer = requireNonNull(serializer); + this.compressor = requireNonNull(compressor); + } + + @Nullable + CharSequence messageEncoding() { + return compressor == null ? null : compressor.encodingName(); + } + + @Override + public void serialize(final T t, final BufferAllocator allocator, final Buffer buffer) { + if (compressor == null) { + final int writerIndexBefore = buffer.writerIndex(); + buffer.writerIndex(writerIndexBefore + METADATA_SIZE); + serializer.serialize(t, allocator, buffer); + buffer.setByte(writerIndexBefore, FLAG_UNCOMPRESSED); + buffer.setInt(writerIndexBefore + 1, buffer.writerIndex() - writerIndexBefore - METADATA_SIZE); + } else { + // First do the serialization. + final int sizeEstimate = serializedBytesEstimator.applyAsInt(t); + Buffer serializedBuffer = allocator.newBuffer(sizeEstimate); + serializer.serialize(t, allocator, serializedBuffer); + + // Compress into the same buffer that we return, so advance the writer index metadata + // bytes and then we fill in the meta data after compression is done and the final size is known. + final int writerIndexBefore = buffer.writerIndex(); + buffer.writerIndex(writerIndexBefore + METADATA_SIZE); + compressor.encoder().serialize(serializedBuffer, allocator, buffer); + buffer.setByte(writerIndexBefore, FLAG_COMPRESSED); + buffer.setInt(writerIndexBefore + 1, buffer.writerIndex() - writerIndexBefore - METADATA_SIZE); + } + } + + @Override + public Buffer serialize(final T t, final BufferAllocator allocator) { + Buffer buffer = allocator.newBuffer(serializedBytesEstimator.applyAsInt(t) + METADATA_SIZE); + serialize(t, allocator, buffer); + return buffer; + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServerBuilder.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServerBuilder.java index ebbd0eebdd..2e17d1b72d 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServerBuilder.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServerBuilder.java @@ -46,6 +46,7 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.internal.FutureUtils.awaitResult; +import static io.servicetalk.grpc.api.GrpcUtils.GRPC_CONTENT_TYPE; import static io.servicetalk.grpc.api.GrpcUtils.newErrorResponse; /** @@ -369,7 +370,8 @@ public Single handle(final HttpServiceContext ctx, private static StreamingHttpResponse convertToGrpcErrorResponse( final HttpServiceContext ctx, final StreamingHttpResponseFactory responseFactory, final Throwable cause) { - return newErrorResponse(responseFactory, null, null, cause, ctx.executionContext().bufferAllocator()); + return newErrorResponse(responseFactory, GRPC_CONTENT_TYPE, cause, + ctx.executionContext().bufferAllocator()); } } } diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServiceContext.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServiceContext.java index f6d06c55e1..ed85669a4a 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServiceContext.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcServiceContext.java @@ -34,9 +34,10 @@ public interface GrpcServiceContext extends ConnectionContext, GrpcMetadata { /** * The {@link ContentCodec} codings available for this gRPC call. - * + * @deprecated Will be removed along with {@link ContentCodec}. * @return the {@link ContentCodec} codings available for this gRPC call. */ + @Deprecated List supportedMessageCodings(); interface GrpcProtocol extends Protocol { diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcStreamingDeserializer.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcStreamingDeserializer.java new file mode 100644 index 0000000000..c9f4e77412 --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcStreamingDeserializer.java @@ -0,0 +1,105 @@ +/* + * Copyright © 2021 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.grpc.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.StreamingDeserializer; +import io.servicetalk.serializer.utils.FramedDeserializerOperator; + +import java.util.function.BiFunction; +import javax.annotation.Nullable; + +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.FLAG_COMPRESSED; +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.FLAG_UNCOMPRESSED; +import static io.servicetalk.grpc.api.GrpcStreamingSerializer.METADATA_SIZE; +import static java.util.Objects.requireNonNull; +import static java.util.function.Function.identity; + +final class GrpcStreamingDeserializer implements StreamingDeserializer { + private final Deserializer serializer; + @Nullable + private final BufferDecoder compressor; + + GrpcStreamingDeserializer(final Deserializer serializer) { + this.serializer = requireNonNull(serializer); + this.compressor = null; + } + + GrpcStreamingDeserializer(final Deserializer serializer, + final BufferDecoder compressor) { + this.serializer = requireNonNull(serializer); + this.compressor = requireNonNull(compressor); + } + + @Nullable + CharSequence messageEncoding() { + return compressor == null ? null : compressor.encodingName(); + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return serializedData.liftSync(new FramedDeserializerOperator<>(serializer, GrpcDeframer::new, allocator)) + .flatMapConcatIterable(identity()); + } + + private final class GrpcDeframer implements BiFunction { + private int expectedLength = -1; + private boolean compressed; + + @Nullable + @Override + public Buffer apply(final Buffer buffer, final BufferAllocator allocator) { + if (expectedLength < 0) { + if (buffer.readableBytes() < METADATA_SIZE) { + return null; + } + compressed = isCompressed(buffer); + if (compressed && compressor == null) { + throw new SerializationException("Compressed flag set, but no compressor"); + } + expectedLength = buffer.readInt(); + if (expectedLength < 0) { + throw new SerializationException("Message-Length invalid: " + expectedLength); + } + } + if (buffer.readableBytes() < expectedLength) { + return null; + } + Buffer result = buffer.readBytes(expectedLength); + expectedLength = -1; + if (compressed) { + assert compressor != null; + return compressor.decoder().deserialize(result, allocator); + } + return result; + } + } + + static boolean isCompressed(Buffer buffer) throws SerializationException { + final byte compressionFlag = buffer.readByte(); + if (compressionFlag == FLAG_UNCOMPRESSED) { + return false; + } else if (compressionFlag == FLAG_COMPRESSED) { + return true; + } + throw new SerializationException("Compression flag must be 0 or 1 but was: " + compressionFlag); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcStreamingSerializer.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcStreamingSerializer.java new file mode 100644 index 0000000000..34ae6c582c --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcStreamingSerializer.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2021 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.grpc.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.StreamingSerializer; + +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * Serializes gRPC Length-Prefixed-Message + * tokens in a stream. + * @param The type of object to serialize. + */ +final class GrpcStreamingSerializer implements StreamingSerializer { + static final int METADATA_SIZE = 5; // 1 byte for compression flag and 4 bytes for length of data + static final byte FLAG_UNCOMPRESSED = 0x0; + static final byte FLAG_COMPRESSED = 0x1; + private final ToIntFunction serializedBytesEstimator; + private final Serializer serializer; + @Nullable + private final BufferEncoder compressor; + + GrpcStreamingSerializer(final ToIntFunction serializedBytesEstimator, + final Serializer serializer) { + this.serializedBytesEstimator = requireNonNull(serializedBytesEstimator); + this.serializer = requireNonNull(serializer); + this.compressor = null; + } + + GrpcStreamingSerializer(final ToIntFunction serializedBytesEstimator, + final Serializer serializer, + final BufferEncoder compressor) { + this.serializedBytesEstimator = requireNonNull(serializedBytesEstimator); + this.serializer = requireNonNull(serializer); + this.compressor = requireNonNull(compressor); + } + + @Nullable + CharSequence messageEncoding() { + return compressor == null ? null : compressor.encodingName(); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return compressor == null ? + toSerialize.map(t -> { + final int sizeEstimate = serializedBytesEstimator.applyAsInt(t); + Buffer buffer = allocator.newBuffer(METADATA_SIZE + sizeEstimate); + final int writerIndexBefore = buffer.writerIndex(); + buffer.writerIndex(writerIndexBefore + METADATA_SIZE); + serializer.serialize(t, allocator, buffer); + buffer.setByte(writerIndexBefore, FLAG_UNCOMPRESSED); + buffer.setInt(writerIndexBefore + 1, buffer.writerIndex() - writerIndexBefore - METADATA_SIZE); + return buffer; + }) : + toSerialize.map(t -> { + // First do the serialization. + final int sizeEstimate = serializedBytesEstimator.applyAsInt(t); + Buffer serializedBuffer = allocator.newBuffer(sizeEstimate); + serializer.serialize(t, allocator, serializedBuffer); + + // Next do the compression, pessimistically assume the size won't decrease when allocating. + Buffer resultBuffer = allocator.newBuffer(METADATA_SIZE + sizeEstimate); + + // Compress into the same buffer that we return, so advance the writer index metadata + // bytes and then we fill in the meta data after compression is done and the final size is known. + final int writerIndexBefore = resultBuffer.writerIndex(); + resultBuffer.writerIndex(writerIndexBefore + METADATA_SIZE); + compressor.encoder().serialize(serializedBuffer, allocator, resultBuffer); + resultBuffer.setByte(writerIndexBefore, FLAG_COMPRESSED); + resultBuffer.setInt(writerIndexBefore + 1, + resultBuffer.writerIndex() - writerIndexBefore - METADATA_SIZE); + return resultBuffer; + }); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcUtils.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcUtils.java index 345c897da8..7505595f26 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcUtils.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/GrpcUtils.java @@ -18,22 +18,31 @@ import io.servicetalk.buffer.api.Buffer; import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import io.servicetalk.encoding.api.Identity; +import io.servicetalk.encoding.api.internal.ContentCodecToBufferDecoder; +import io.servicetalk.encoding.api.internal.ContentCodecToBufferEncoder; import io.servicetalk.encoding.api.internal.HeaderUtils; +import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.HttpDeserializer; import io.servicetalk.http.api.HttpHeaders; -import io.servicetalk.http.api.HttpMetaData; import io.servicetalk.http.api.HttpRequestMetaData; import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpResponseFactory; import io.servicetalk.http.api.HttpResponseMetaData; -import io.servicetalk.http.api.HttpResponseStatus; import io.servicetalk.http.api.HttpSerializer; import io.servicetalk.http.api.StatelessTrailersTransformer; import io.servicetalk.http.api.StreamingHttpResponse; import io.servicetalk.http.api.StreamingHttpResponseFactory; import io.servicetalk.http.api.TrailersTransformer; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.SerializerDeserializer; import com.google.protobuf.InvalidProtocolBufferException; import com.google.rpc.Status; @@ -41,18 +50,21 @@ import org.slf4j.LoggerFactory; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeoutException; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.function.ToIntFunction; import javax.annotation.Nullable; +import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase; import static io.servicetalk.buffer.api.CharSequences.newAsciiString; +import static io.servicetalk.buffer.api.CharSequences.regionMatches; import static io.servicetalk.encoding.api.Identity.identity; -import static io.servicetalk.encoding.api.internal.HeaderUtils.encodingFor; +import static io.servicetalk.encoding.api.internal.HeaderUtils.encodingForRaw; import static io.servicetalk.grpc.api.GrpcStatusCode.CANCELLED; import static io.servicetalk.grpc.api.GrpcStatusCode.DEADLINE_EXCEEDED; import static io.servicetalk.grpc.api.GrpcStatusCode.INTERNAL; @@ -60,7 +72,6 @@ import static io.servicetalk.grpc.api.GrpcStatusCode.UNKNOWN; import static io.servicetalk.grpc.internal.DeadlineUtils.GRPC_TIMEOUT_HEADER_KEY; import static io.servicetalk.grpc.internal.DeadlineUtils.makeTimeoutHeader; -import static io.servicetalk.http.api.HeaderUtils.hasContentType; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; import static io.servicetalk.http.api.HttpHeaderNames.SERVER; import static io.servicetalk.http.api.HttpHeaderNames.TE; @@ -68,12 +79,14 @@ import static io.servicetalk.http.api.HttpHeaderValues.TRAILERS; import static io.servicetalk.http.api.HttpRequestMethod.POST; import static java.lang.String.valueOf; +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; final class GrpcUtils { private static final Logger LOGGER = LoggerFactory.getLogger(GrpcUtils.class); - - // TODO could/should add "+proto" - private static final CharSequence GRPC_CONTENT_TYPE = newAsciiString("application/grpc"); + private static final String GRPC_CONTENT_TYPE_PREFIX = "application/grpc"; + static final String GRPC_PROTO_CONTENT_TYPE = "+proto"; + static final CharSequence GRPC_CONTENT_TYPE = newAsciiString(GRPC_CONTENT_TYPE_PREFIX); private static final CharSequence GRPC_STATUS_CODE_TRAILER = newAsciiString("grpc-status"); private static final CharSequence GRPC_STATUS_DETAILS_TRAILER = newAsciiString("grpc-status-details-bin"); private static final CharSequence GRPC_STATUS_MESSAGE_TRAILER = newAsciiString("grpc-message"); @@ -82,9 +95,7 @@ final class GrpcUtils { private static final CharSequence GRPC_MESSAGE_ENCODING_KEY = newAsciiString("grpc-encoding"); private static final CharSequence GRPC_ACCEPT_ENCODING_KEY = newAsciiString("grpc-accept-encoding"); private static final GrpcStatus STATUS_OK = GrpcStatus.fromCodeValue(GrpcStatusCode.OK.value()); - private static final ConcurrentMap, CharSequence> ENCODINGS_HEADER_CACHE = - new ConcurrentHashMap<>(); - private static final CharSequence CONTENT_ENCODING_SEPARATOR = ", "; + private static final BufferDecoderGroup EMPTY_BUFFER_DECODER_GROUP = new BufferDecoderGroupBuilder().build(); private static final TrailersTransformer ENSURE_GRPC_STATUS_RECEIVED = new StatelessTrailersTransformer() { @@ -123,7 +134,9 @@ private GrpcUtils() { } static void initRequest(final HttpRequestMetaData request, - final List supportedEncodings, + final CharSequence contentType, + @Nullable final CharSequence encoding, + @Nullable final CharSequence acceptedEncoding, @Nullable final Duration timeout) { assert POST.equals(request.method()); final HttpHeaders headers = request.headers(); @@ -133,84 +146,86 @@ static void initRequest(final HttpRequestMetaData request, } headers.set(USER_AGENT, GRPC_USER_AGENT); headers.set(TE, TRAILERS); - headers.set(CONTENT_TYPE, GRPC_CONTENT_TYPE); - final CharSequence acceptedEncoding = acceptedEncodingsHeaderValueOrCached(supportedEncodings); + headers.set(CONTENT_TYPE, contentType); + if (encoding != null) { + headers.set(GRPC_MESSAGE_ENCODING_KEY, encoding); + } if (acceptedEncoding != null) { headers.set(GRPC_ACCEPT_ENCODING_KEY, acceptedEncoding); } } static StreamingHttpResponse newResponse(final StreamingHttpResponseFactory responseFactory, - @Nullable final GrpcServiceContext context, + final CharSequence contentType, + @Nullable final CharSequence encoding, + @Nullable final CharSequence acceptedEncoding, final Publisher payload, - final HttpSerializer serializer, + final GrpcStreamingSerializer serializer, final BufferAllocator allocator) { - return newStreamingResponse(responseFactory, context).payloadBody(payload, serializer) + return newStreamingResponse(responseFactory, contentType, encoding, acceptedEncoding) + .payloadBody(serializer.serialize(payload, allocator)) .transform(new GrpcStatusUpdater(allocator, STATUS_OK)); } static StreamingHttpResponse newResponse(final StreamingHttpResponseFactory responseFactory, - @Nullable final GrpcServiceContext context, + final CharSequence contentType, + @Nullable final CharSequence encoding, + @Nullable final CharSequence acceptedEncoding, final GrpcStatus status, final BufferAllocator allocator) { - return newStreamingResponse(responseFactory, context).transform(new GrpcStatusUpdater(allocator, status)); + return newStreamingResponse(responseFactory, contentType, encoding, acceptedEncoding) + .transform(new GrpcStatusUpdater(allocator, status)); } static HttpResponse newResponse(final HttpResponseFactory responseFactory, - @Nullable final GrpcServiceContext context, - final BufferAllocator allocator) { + final CharSequence contentType, + @Nullable final CharSequence encoding, + @Nullable final CharSequence acceptedEncoding) { final HttpResponse response = responseFactory.ok(); - initResponse(response, context); - setStatusOk(response.trailers(), allocator); + initResponse(response, contentType, encoding, acceptedEncoding); + setStatusOk(response.trailers()); return response; } - static HttpResponse newErrorResponse( - final HttpResponseFactory responseFactory, @Nullable final GrpcServiceContext context, - @Nullable final GrpcStatus status, @Nullable final Throwable cause, final BufferAllocator allocator) { - assert status != null || cause != null; + static HttpResponse newErrorResponse(final HttpResponseFactory responseFactory, + final CharSequence contentType, + final Throwable cause, final BufferAllocator allocator) { final HttpResponse response = responseFactory.ok(); - initResponse(response, context); - if (status != null) { - setStatus(response.headers(), status, null, allocator); - } else { - setStatus(response.headers(), cause, allocator); - } + initResponse(response, contentType, null, null); + setStatus(response.headers(), cause, allocator); return response; } - static StreamingHttpResponse newErrorResponse( - final StreamingHttpResponseFactory responseFactory, @Nullable final GrpcServiceContext context, - @Nullable final GrpcStatus status, @Nullable final Throwable cause, final BufferAllocator allocator) { - assert (status != null && cause == null) || (status == null && cause != null); + static StreamingHttpResponse newErrorResponse(final StreamingHttpResponseFactory responseFactory, + final CharSequence contentType, final Throwable cause, + final BufferAllocator allocator) { final StreamingHttpResponse response = responseFactory.ok(); - initResponse(response, context); - if (status != null) { - setStatus(response.headers(), status, null, allocator); - } else { - setStatus(response.headers(), cause, allocator); - } + initResponse(response, contentType, null, null); + setStatus(response.headers(), cause, allocator); return response; } private static StreamingHttpResponse newStreamingResponse(final StreamingHttpResponseFactory responseFactory, - @Nullable final GrpcServiceContext context) { + final CharSequence contentType, + @Nullable final CharSequence encoding, + @Nullable final CharSequence acceptedEncoding) { final StreamingHttpResponse response = responseFactory.ok(); - initResponse(response, context); + initResponse(response, contentType, encoding, acceptedEncoding); return response; } - static void setStatusOk(final HttpHeaders trailers, final BufferAllocator allocator) { - setStatus(trailers, STATUS_OK, null, allocator); + static void setStatusOk(final HttpHeaders trailers) { + setStatus(trailers, STATUS_OK, null, null); } static void setStatus(final HttpHeaders trailers, final GrpcStatus status, @Nullable final Status details, - final BufferAllocator allocator) { + @Nullable final BufferAllocator allocator) { trailers.set(GRPC_STATUS_CODE_TRAILER, valueOf(status.code().value())); if (status.description() != null) { trailers.set(GRPC_STATUS_MESSAGE_TRAILER, status.description()); } if (details != null) { + assert allocator != null; trailers.set(GRPC_STATUS_DETAILS_TRAILER, newAsciiString(allocator.wrap(Base64.getEncoder().encode(details.toByteArray())))); } @@ -231,6 +246,8 @@ static GrpcStatus toGrpcStatus(Throwable cause) { MessageEncodingException msgEncException = (MessageEncodingException) cause; status = new GrpcStatus(UNIMPLEMENTED, cause, "Message encoding '" + msgEncException.encoding() + "' not supported "); + } else if (cause instanceof SerializationException) { + status = new GrpcStatus(UNKNOWN, cause, "Serialization error: " + cause.getMessage()); } else if (cause instanceof CancellationException) { status = new GrpcStatus(CANCELLED, cause); } else if (cause instanceof TimeoutException) { @@ -250,14 +267,15 @@ static GrpcStatusException toGrpcException(Throwable cause) { } static Publisher validateResponseAndGetPayload(final StreamingHttpResponse response, - final HttpDeserializer deserializer) { + final CharSequence expectedContentType, + final BufferAllocator allocator, + final GrpcStreamingDeserializer deserializer) { // In case of an empty response, gRPC-server may return only one HEADER frame with endStream=true. Our // HTTP1-based implementation translates them into response headers so we need to look for a grpc-status in both // headers and trailers. Since this is streaming response and we have the headers now, we check for the // grpc-status here first. If there is no grpc-status in headers, we look for it in trailers later. - final HttpHeaders headers = response.headers(); - ensureGrpcContentType(response.status(), headers); + validateContentType(headers, expectedContentType); final GrpcStatusCode grpcStatusCode = extractGrpcStatusCodeFromHeaders(headers); if (grpcStatusCode != null) { final GrpcStatusException grpcStatusException = convertToGrpcStatusException(grpcStatusCode, headers); @@ -271,17 +289,19 @@ static Publisher validateResponseAndGetPayload(final StreamingHttpR } response.transform(ENSURE_GRPC_STATUS_RECEIVED); - return deserializer.deserialize(headers, response.payloadBody()); + return deserializer.deserialize(response.payloadBody(), allocator); } static Resp validateResponseAndGetPayload(final HttpResponse response, - final HttpDeserializer deserializer) { + final CharSequence expectedContentType, + final BufferAllocator allocator, + final GrpcDeserializer deserializer) { // In case of an empty response, gRPC-server may return only one HEADER frame with endStream=true. Our // HTTP1-based implementation translates them into response headers so we need to look for a grpc-status in both // headers and trailers. final HttpHeaders headers = response.headers(); final HttpHeaders trailers = response.trailers(); - ensureGrpcContentType(response.status(), headers); + validateContentType(headers, expectedContentType); // We will try the trailers first as this is the most likely place to find the gRPC-related headers. final GrpcStatusCode grpcStatusCode = extractGrpcStatusCodeFromHeaders(trailers); @@ -290,25 +310,28 @@ static Resp validateResponseAndGetPayload(final HttpResponse response, if (grpcStatusException != null) { throw grpcStatusException; } - return response.payloadBody(deserializer); + return deserializer.deserialize(response.payloadBody(), allocator); } // There was no grpc-status in the trailers, so it must be in headers. ensureGrpcStatusReceived(headers); - return response.payloadBody(deserializer); + return deserializer.deserialize(response.payloadBody(), allocator); } - private static void ensureGrpcContentType(final HttpResponseStatus status, - final HttpHeaders headers) { - final CharSequence contentTypeHeader = headers.get(CONTENT_TYPE); - if (!hasContentType(headers, GRPC_CONTENT_TYPE, null)) { - throw new GrpcStatus(INTERNAL, null, - "HTTP status code: " + status + "\n" + - "\tinvalid " + CONTENT_TYPE + ": " + contentTypeHeader + "\n" + - "\theaders: " + headers).asException(); + static void validateContentType(HttpHeaders headers, CharSequence expectedContentType) { + CharSequence requestContentType = headers.get(CONTENT_TYPE); + if (!contentEqualsIgnoreCase(requestContentType, expectedContentType) && + (requestContentType == null || + !regionMatches(requestContentType, true, 0, GRPC_CONTENT_TYPE, 0, GRPC_CONTENT_TYPE.length()))) { + throw GrpcStatusException.of(Status.newBuilder().setCode(INTERNAL.value()) + .setMessage("invalid content-type: " + requestContentType).build()); } } + static CharSequence grpcContentType(CharSequence contentType) { + return newAsciiString(GRPC_CONTENT_TYPE_PREFIX + contentType); + } + private static void ensureGrpcStatusReceived(final HttpHeaders headers) { final GrpcStatusCode statusCode = extractGrpcStatusCodeFromHeaders(headers); if (statusCode == null) { @@ -322,54 +345,44 @@ private static void ensureGrpcStatusReceived(final HttpHeaders headers) { } } - static ContentCodec readGrpcMessageEncoding(final HttpMetaData httpMetaData, - final List allowedEncodings) { - final CharSequence encoding = httpMetaData.headers().get(GRPC_MESSAGE_ENCODING_KEY); + static T readGrpcMessageEncodingRaw(final HttpHeaders headers, final T identityEncoder, + final List supportedEncoders, + final Function messageEncodingFunc) { + final CharSequence encoding = headers.get(GRPC_MESSAGE_ENCODING_KEY); if (encoding == null) { - return identity(); + return identityEncoder; } - - ContentCodec enc = encodingFor(allowedEncodings, encoding); - if (enc == null) { - throw new MessageEncodingException(encoding.toString()); + final T result = encodingForRaw(supportedEncoders, messageEncodingFunc, encoding); + if (result == null) { + throw GrpcStatusException.of(Status.newBuilder().setCode(UNIMPLEMENTED.value()) + .setMessage("Invalid " + GRPC_MESSAGE_ENCODING_KEY + ": " + encoding).build()); } - return enc; + return result; } - /** - * Establish a commonly accepted encoding between server and client, according to the supported-codings - * on the server side and the {@code 'Accepted-Encoding'} incoming header on the request. - *

- * If no supported codings are configured then the result is always {@code identity} - * If no accepted codings are present in the request then the result is always {@code identity} - * In all other cases, the first matching encoding (that is NOT {@link Identity#identity()}) is preferred, - * otherwise {@code identity} is returned. - * - * @param httpMetaData The client metadata to extract relevant headers from. - * @param allowedCodings The server supported codings as configured. - * @return The {@link ContentCodec} that satisfies both client and server needs, identity otherwise. - */ - static ContentCodec negotiateAcceptedEncoding( - final HttpMetaData httpMetaData, - final List allowedCodings) { - - CharSequence acceptEncHeaderValue = httpMetaData.headers().get(GRPC_ACCEPT_ENCODING_KEY); - ContentCodec encoding = HeaderUtils.negotiateAcceptedEncoding(acceptEncHeaderValue, allowedCodings); - return encoding == null ? identity() : encoding; + static T negotiateAcceptedEncodingRaw(final HttpHeaders headers, + final T identityEncoder, + final List supportedEncoders, + final Function messageEncodingFunc) { + T result = HeaderUtils.negotiateAcceptedEncodingRaw(headers.get(GRPC_ACCEPT_ENCODING_KEY), + supportedEncoders, messageEncodingFunc); + return result == null ? identityEncoder : result; } - private static void initResponse(final HttpResponseMetaData response, @Nullable final GrpcServiceContext context) { + static void initResponse(final HttpResponseMetaData response, + final CharSequence contentType, + @Nullable final CharSequence encoding, + @Nullable final CharSequence acceptedEncoding) { // The response status is 200 no matter what. Actual status is put in trailers. final HttpHeaders headers = response.headers(); headers.set(SERVER, GRPC_USER_AGENT); - headers.set(CONTENT_TYPE, GRPC_CONTENT_TYPE); - if (context != null) { - final CharSequence acceptedEncoding = - acceptedEncodingsHeaderValueOrCached(context.supportedMessageCodings()); - if (acceptedEncoding != null) { - headers.set(GRPC_ACCEPT_ENCODING_KEY, acceptedEncoding); - } + headers.set(CONTENT_TYPE, contentType); + if (encoding != null) { + headers.set(GRPC_MESSAGE_ENCODING_KEY, encoding); + } + if (acceptedEncoding != null) { + headers.set(GRPC_ACCEPT_ENCODING_KEY, acceptedEncoding); } } @@ -406,39 +419,261 @@ private static Status getStatusDetails(final HttpHeaders headers) { } } - /** - * Construct the gRPC header {@code grpc-accept-encoding} representation of the given context. - * - * @param codings the list of codings to be used in the string representation. - * @return a comma separated string representation of the codings for use as a header value or {@code null} if - * the only supported coding is {@link Identity#identity()}. - */ - @Nullable - private static CharSequence acceptedEncodingsHeaderValueOrCached(final List codings) { - return ENCODINGS_HEADER_CACHE.computeIfAbsent(codings, (__) -> acceptedEncodingsHeaderValue0(codings)); + static ToIntFunction defaultToInt() { + return t -> 256; } - @Nullable - private static CharSequence acceptedEncodingsHeaderValue0(final List codings) { - StringBuilder builder = new StringBuilder(codings.size() * (12 + CONTENT_ENCODING_SEPARATOR.length())); - for (ContentCodec codec : codings) { - if (identity().equals(codec)) { - continue; - } - builder.append(codec.name()).append(CONTENT_ENCODING_SEPARATOR); + static List> streamingDeserializers(Deserializer desrializer, + List decompressors) { + if (decompressors.isEmpty()) { + return emptyList(); + } + List> deserializers = new ArrayList<>(decompressors.size()); + for (BufferDecoder decompressor : decompressors) { + deserializers.add(new GrpcStreamingDeserializer<>(desrializer, decompressor)); + } + + return deserializers; + } + + static List> streamingSerializers(SerializerDeserializer serializer, + ToIntFunction bytesEstimator, + List compressors) { + if (compressors.isEmpty()) { + return emptyList(); + } + List> serializers = new ArrayList<>(compressors.size()); + for (BufferEncoder compressor : compressors) { + serializers.add(new GrpcStreamingSerializer<>(bytesEstimator, serializer, compressor)); } + return serializers; + } + + static List> deserializers(Deserializer deserializer, + List decompressors) { + if (decompressors.isEmpty()) { + return emptyList(); + } + List> deserializers = new ArrayList<>(decompressors.size()); + for (BufferDecoder decompressor : decompressors) { + deserializers.add(new GrpcDeserializer<>(deserializer, decompressor)); + } + + return deserializers; + } + + static List> serializers(Serializer serializer, ToIntFunction byteEstimator, + List compressors) { + if (compressors.isEmpty()) { + return emptyList(); + } + List> serializers = new ArrayList<>(compressors.size()); + for (BufferEncoder compressor : compressors) { + serializers.add(new GrpcSerializer<>(byteEstimator, serializer, compressor)); + } + return serializers; + } + + @Deprecated + static SerializerDeserializer serializerDeserializer( + final GrpcSerializationProvider serializationProvider, Class clazz) { + return new HttpSerializerToSerializer<>(serializationProvider.serializerFor(Identity.identity(), clazz), + serializationProvider.deserializerFor(Identity.identity(), clazz)); + } - if (builder.length() > CONTENT_ENCODING_SEPARATOR.length()) { - builder.setLength(builder.length() - CONTENT_ENCODING_SEPARATOR.length()); - return newAsciiString(builder); + @Deprecated + static List compressors(List codecs) { + if (codecs.isEmpty()) { + return emptyList(); + } + List encoders = new ArrayList<>(codecs.size()); + for (ContentCodec codec : codecs) { + encoders.add(new ContentCodecToBufferEncoder(codec)); } + return encoders; + } - return null; + @Deprecated + static BufferDecoderGroup decompressors(List codecs) { + if (codecs.isEmpty()) { + return EMPTY_BUFFER_DECODER_GROUP; + } + BufferDecoderGroupBuilder builder = new BufferDecoderGroupBuilder(codecs.size()); + for (ContentCodec codec : codecs) { + builder.add(new ContentCodecToBufferDecoder(codec), codec != identity()); + } + return builder.build(); } - @SuppressWarnings("unchecked") - static T uncheckedCast(Object o) { - return (T) o; + @Deprecated + static final class HttpSerializerToSerializer implements SerializerDeserializer { + private final HttpSerializer httpSerializer; + private final HttpDeserializer httpDeserializer; + + HttpSerializerToSerializer(HttpSerializer httpSerializer, HttpDeserializer httpDeserializer) { + this.httpSerializer = requireNonNull(httpSerializer); + this.httpDeserializer = requireNonNull(httpDeserializer); + } + + @Override + public T deserialize(final Buffer serializedData, final BufferAllocator allocator) { + // Re-apply the gRPC framing that was previously stripped. Previously the gRPC framing was understood and + // parsed by the external HttpDeserializer. + Buffer wrappedBuffer = allocator.newBuffer(serializedData.readableBytes() + 5); + wrappedBuffer.writeByte(0); // Compression is applied at a higher level now with the new APIs. + wrappedBuffer.writeInt(serializedData.readableBytes()); + wrappedBuffer.writeBytes(serializedData); + return httpDeserializer.deserialize(DefaultHttpHeadersFactory.INSTANCE.newHeaders(), wrappedBuffer); + } + + @Override + public void serialize(final T toSerialize, final BufferAllocator allocator, final Buffer buffer) { + // Skip gRPC payload framing applied externally, because it is now applied internally. + Buffer httpResult = httpSerializer.serialize(DefaultHttpHeadersFactory.INSTANCE.newHeaders(), toSerialize, + allocator); + buffer.writeBytes(httpResult, httpResult.readerIndex() + 5, httpResult.readableBytes() - 5); + } + } + + static final class DefaultParameterDescriptor implements ParameterDescriptor { + private final boolean isStreaming; + private final boolean isAsync; + private final Class parameterClass; + private final SerializerDescriptor serializerDescriptor; + + DefaultParameterDescriptor(final boolean isStreaming, final boolean isAsync, final Class parameterClass, + final SerializerDescriptor serializerDescriptor) { + this.isStreaming = isStreaming; + this.isAsync = isAsync; + this.parameterClass = parameterClass; + this.serializerDescriptor = serializerDescriptor; + } + + @Override + public boolean isStreaming() { + return isStreaming; + } + + @Override + public boolean isAsync() { + return isAsync; + } + + @Override + public Class parameterClass() { + return parameterClass; + } + + @Override + public SerializerDescriptor serializerDescriptor() { + return serializerDescriptor; + } + } + + static final class DefaultSerializerDescriptor implements SerializerDescriptor { + private final CharSequence contentType; + private final SerializerDeserializer serializer; + private final ToIntFunction bytesEstimator; + + DefaultSerializerDescriptor(final CharSequence contentType, final SerializerDeserializer serializer, + final ToIntFunction bytesEstimator) { + this.contentType = requireNonNull(contentType); + this.serializer = requireNonNull(serializer); + this.bytesEstimator = requireNonNull(bytesEstimator); + } + + @Override + public CharSequence contentType() { + return contentType; + } + + @Override + public SerializerDeserializer serializer() { + return serializer; + } + + @Override + public ToIntFunction bytesEstimator() { + return bytesEstimator; + } + } + + static final class DefaultMethodDescriptor implements MethodDescriptor { + private final String httpPath; + private final String javaMethodName; + private final ParameterDescriptor requestDescriptor; + private final ParameterDescriptor responseDescriptor; + + @Deprecated + DefaultMethodDescriptor(final String httpPath, final boolean reqIsStreaming, + final boolean reqIsAsync, final Class reqClass, final CharSequence reqContentType, + final SerializerDeserializer reqSerializer, + final ToIntFunction reqBytesEstimator, final boolean respIsStreaming, + final boolean respIsAsync, final Class respClass, + final CharSequence respContentType, + final SerializerDeserializer respSerializer, + final ToIntFunction respBytesEstimator) { + this(httpPath, extractJavaMethodName(httpPath), reqIsStreaming, reqIsAsync, reqClass, reqContentType, + reqSerializer, reqBytesEstimator, respIsStreaming, respIsAsync, respClass, respContentType, + respSerializer, respBytesEstimator); + } + + DefaultMethodDescriptor(final String httpPath, final String javaMethodName, final boolean reqIsStreaming, + final boolean reqIsAsync, final Class reqClass, final CharSequence reqContentType, + final SerializerDeserializer reqSerializer, + final ToIntFunction reqBytesEstimator, final boolean respIsStreaming, + final boolean respIsAsync, final Class respClass, + final CharSequence respContentType, + final SerializerDeserializer respSerializer, + final ToIntFunction respBytesEstimator) { + this(httpPath, javaMethodName, + new DefaultParameterDescriptor<>(reqIsStreaming, reqIsAsync, reqClass, + new DefaultSerializerDescriptor<>(reqContentType, reqSerializer, reqBytesEstimator)), + new DefaultParameterDescriptor<>(respIsStreaming, respIsAsync, respClass, + new DefaultSerializerDescriptor<>(respContentType, respSerializer, respBytesEstimator))); + } + + private DefaultMethodDescriptor(final String httpPath, final String javaMethodName, + final ParameterDescriptor requestDescriptor, + final ParameterDescriptor responseDescriptor) { + this.httpPath = requireNonNull(httpPath); + this.javaMethodName = requireNonNull(javaMethodName); + this.requestDescriptor = requireNonNull(requestDescriptor); + this.responseDescriptor = requireNonNull(responseDescriptor); + } + + private static String extractJavaMethodName(String httpPath) { + int i = httpPath.lastIndexOf('/'); + if (i < 0) { + return ""; + } + String result = httpPath.substring(i + 1); + final char firstChar; + if (result.isEmpty() || Character.isLowerCase((firstChar = result.charAt(0)))) { + return result; + } + return Character.toLowerCase(firstChar) + result.substring(1); + } + + @Override + public String httpPath() { + return httpPath; + } + + @Override + public String javaMethodName() { + return javaMethodName; + } + + @Override + public ParameterDescriptor requestDescriptor() { + return requestDescriptor; + } + + @Override + public ParameterDescriptor responseDescriptor() { + return responseDescriptor; + } } /** diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/MethodDescriptor.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/MethodDescriptor.java new file mode 100644 index 0000000000..94e4b3b378 --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/MethodDescriptor.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2021 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.grpc.api; + +/** + * Metadata that describes a specific RPC method. + * @param The type of request. + * @param The type of response. + */ +public interface MethodDescriptor { + /** + * Get the + * HTTP Path used by this + * method. + * @return The + * HTTP Path used by this + * method. + */ + String httpPath(); + + /** + * Get the java method name corresponding to this method. + * @return the java method name corresponding to this method. + */ + String javaMethodName(); + + /** + * Get the {@link ParameterDescriptor} for the request. + * @return the {@link ParameterDescriptor} for the request. + */ + ParameterDescriptor requestDescriptor(); + + /** + * Get the {@link ParameterDescriptor} for the response. + * @return the {@link ParameterDescriptor} for the response. + */ + ParameterDescriptor responseDescriptor(); +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/MethodDescriptors.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/MethodDescriptors.java new file mode 100644 index 0000000000..0ebc8571a5 --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/MethodDescriptors.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2021 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.grpc.api; + +import io.servicetalk.serializer.api.SerializerDeserializer; + +import java.util.function.ToIntFunction; + +/** + * Utility methods for {@link MethodDescriptor}. + */ +public final class MethodDescriptors { + private MethodDescriptors() { + } + + /** + * Create a new {@link MethodDescriptor}. + * @param httpPath See {@link MethodDescriptor#httpPath()}. + * @param javaMethodName See {@link MethodDescriptor#javaMethodName()}. + * @param reqIsStreaming {@link ParameterDescriptor#isStreaming()} for the request. + * @param reqIsAsync {@link ParameterDescriptor#isAsync()} for the request. + * @param reqClass {@link ParameterDescriptor#parameterClass()} for the request. + * @param reqContentType {@link SerializerDescriptor#contentType()} for the request. + * @param reqSerializer {@link SerializerDescriptor#serializer()} for the request. + * @param reqBytesEstimator {@link SerializerDescriptor#bytesEstimator()} for the request. + * @param respIsStreaming {@link ParameterDescriptor#isStreaming()} for the response. + * @param respIsAsync {@link ParameterDescriptor#isAsync()} for the response. + * @param respClass {@link ParameterDescriptor#parameterClass()} for the response. + * @param respContentType {@link SerializerDescriptor#contentType()} for the response. + * @param respSerializer {@link SerializerDescriptor#serializer()} for the response. + * @param respBytesEstimator {@link SerializerDescriptor#bytesEstimator()} for the response. + * @param The request type. + * @param The response type. + * @return A {@link MethodDescriptor} as described by all parameters. + */ + public static MethodDescriptor newMethodDescriptor( + final String httpPath, final String javaMethodName, final boolean reqIsStreaming, final boolean reqIsAsync, + final Class reqClass, final CharSequence reqContentType, + final SerializerDeserializer reqSerializer, final ToIntFunction reqBytesEstimator, + final boolean respIsStreaming, final boolean respIsAsync, final Class respClass, + final CharSequence respContentType, final SerializerDeserializer respSerializer, + final ToIntFunction respBytesEstimator) { + return new GrpcUtils.DefaultMethodDescriptor<>(httpPath, javaMethodName, reqIsStreaming, reqIsAsync, reqClass, + reqContentType, reqSerializer, reqBytesEstimator, respIsStreaming, respIsAsync, respClass, + respContentType, respSerializer, respBytesEstimator); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/ParameterDescriptor.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/ParameterDescriptor.java new file mode 100644 index 0000000000..067c7dca3f --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/ParameterDescriptor.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2021 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.grpc.api; + +/** + * Description of a parameter or return value related to a {@link MethodDescriptor}. + * @param The type of the parameter. + */ +public interface ParameterDescriptor { + /** + * Determine if the parameter type is streaming or scalar. + * @return {@code true} if the parameter is streaming. {@code false} if the parameter is scalar. + */ + boolean isStreaming(); + + /** + * Determine if the parameter type is asynchronous. + * @return {@code true} if the parameter type is asynchronous. {@code false} if the parameter type is synchronous. + */ + boolean isAsync(); + + /** + * Get the java {@link Class} for the parameter type. + * @return the java {@link Class} for the parameter type. + */ + Class parameterClass(); + + /** + * Get the {@link SerializerDescriptor} for this parameter. + * @return the {@link SerializerDescriptor} for this parameter. + */ + SerializerDescriptor serializerDescriptor(); +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/SerializerDescriptor.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/SerializerDescriptor.java new file mode 100644 index 0000000000..4880cfe4dd --- /dev/null +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/SerializerDescriptor.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2021 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.grpc.api; + +import io.servicetalk.serializer.api.SerializerDeserializer; + +import java.util.function.ToIntFunction; + +/** + * Description of the serialization used for individual elements related to a {@link ParameterDescriptor}. + * @param The type being serialized. + */ +public interface SerializerDescriptor { + /** + * Get the suffix to application/grpc which described the + * Content-Type for the + * serialization. For example: +proto. + * @return the suffix to application/grpc which described the + * Content-Type for the + * serialization. For example: +proto. + */ + CharSequence contentType(); + + /** + * Get the {@link SerializerDeserializer} used to serialize and deserialize each object. + * @return the {@link SerializerDeserializer} used to serialize and deserialize each object. + */ + SerializerDeserializer serializer(); + + /** + * Get a function used to estimate the serialized size (in bytes) of each object. This is used to provide an + * estimate to pre-allocate memory to serialize into. + * @return a function used to estimate the serialized size (in bytes) of each object. + */ + ToIntFunction bytesEstimator(); +} diff --git a/servicetalk-grpc-netty/build.gradle b/servicetalk-grpc-netty/build.gradle index 847109cb82..7734436fa7 100644 --- a/servicetalk-grpc-netty/build.gradle +++ b/servicetalk-grpc-netty/build.gradle @@ -45,6 +45,7 @@ dependencies { testImplementation project(":servicetalk-encoding-api-internal") testImplementation project(":servicetalk-encoding-netty") testImplementation project(":servicetalk-grpc-protobuf") + testImplementation project(":servicetalk-data-protobuf") testImplementation project(":servicetalk-grpc-protoc") testImplementation project(":servicetalk-router-utils-internal") testImplementation project(":servicetalk-test-resources") diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientRequiresTrailersTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientRequiresTrailersTest.java index 4165b17a9b..580e51420f 100644 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientRequiresTrailersTest.java +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientRequiresTrailersTest.java @@ -16,38 +16,34 @@ package io.servicetalk.grpc.netty; import io.servicetalk.buffer.api.Buffer; -import io.servicetalk.grpc.api.GrpcSerializationProvider; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.api.GrpcStatusCode; import io.servicetalk.grpc.api.GrpcStatusException; import io.servicetalk.grpc.netty.TesterProto.TestRequest; import io.servicetalk.grpc.netty.TesterProto.TestResponse; import io.servicetalk.grpc.netty.TesterProto.Tester.BlockingTesterClient; -import io.servicetalk.grpc.protobuf.ProtoBufSerializationProviderBuilder; import io.servicetalk.http.api.HttpHeaders; -import io.servicetalk.http.api.HttpServerBuilder; +import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StatelessTrailersTransformer; +import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; -import io.servicetalk.http.api.StreamingHttpService; -import io.servicetalk.http.netty.HttpServers; +import io.servicetalk.http.api.StreamingHttpResponseFactory; +import io.servicetalk.http.api.StreamingHttpServiceFilter; import io.servicetalk.transport.api.ServerContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.concurrent.ExecutionException; -import java.util.stream.Stream; import javax.annotation.Nullable; import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.encoding.api.Identity.identity; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.noOffloadsStrategy; -import static io.servicetalk.http.api.HttpApiConversions.toHttpService; -import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; -import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static java.util.Collections.singletonList; @@ -58,54 +54,61 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class GrpcClientRequiresTrailersTest { - - private static final GrpcSerializationProvider SERIALIZATION_PROVIDER = new ProtoBufSerializationProviderBuilder() - .registerMessageType(TestRequest.class, TestRequest.parser()) - .registerMessageType(TestResponse.class, TestResponse.parser()) - .build(); - @Nullable private ServerContext serverContext; @Nullable private BlockingTesterClient client; - private void setUp(boolean streaming, boolean hasTrailers) throws Exception { - // Emulate gRPC server that does not add grpc-status to the trailers after payload body: - StreamingHttpService streamingService = (ctx, request, responseFactory) -> { - final StreamingHttpResponse response = responseFactory.ok() - .version(request.version()) - .setHeader(CONTENT_TYPE, "application/grpc") - .payloadBody(from(TestResponse.newBuilder().setMessage("response").build()), - SERIALIZATION_PROVIDER.serializerFor(identity(), TestResponse.class)); - - if (hasTrailers) { - response.transform(new StatelessTrailersTransformer() { + private void setUp(boolean hasTrailers) throws Exception { + serverContext = GrpcServers.forAddress(localAddress(0)) + .appendHttpServiceFilter(service -> new StreamingHttpServiceFilter(service) { @Override - protected HttpHeaders payloadComplete(final HttpHeaders trailers) { - return trailers.set("some-trailer", "some-value"); + public Single handle( + final HttpServiceContext ctx, final StreamingHttpRequest request, + final StreamingHttpResponseFactory responseFactory) { + return delegate().handle(ctx, request, responseFactory).map(resp -> { + // Emulate gRPC server that does not add grpc-status to the trailers after payload body: + resp.transform(new StatelessTrailersTransformer() { + @Override + protected HttpHeaders payloadComplete(final HttpHeaders trailers) { + trailers.remove("grpc-status"); + return hasTrailers ? trailers.set("some-trailer", "some-value") : trailers; + } + }); + return resp; + }); + } + }) + .listenAndAwait(new TesterProto.Tester.TesterService() { + @Override + public Single testRequestStream(final GrpcServiceContext ctx, + final Publisher request) { + return succeeded(newResponse()); + } + + @Override + public Publisher testResponseStream(final GrpcServiceContext ctx, + final TestRequest request) { + return from(newResponse()); + } + + @Override + public Publisher testBiDiStream(final GrpcServiceContext ctx, + final Publisher request) { + return from(newResponse()); + } + + @Override + public Single test(final GrpcServiceContext ctx, final TestRequest request) { + return succeeded(newResponse()); } }); - } - return succeeded(response); - }; - HttpServerBuilder serverBuilder = HttpServers.forAddress(localAddress(0)) - .protocols(h2Default()); - serverContext = streaming ? serverBuilder.listenStreamingAndAwait(streamingService) : - serverBuilder.listenAndAwait(toHttpService(streamingService)); client = GrpcClients.forAddress(serverHostAndPort(serverContext)) .executionStrategy(noOffloadsStrategy()) .buildBlocking(new TesterProto.Tester.ClientFactory()); } - static Stream params() { - return Stream.of( - Arguments.of(false, false), - Arguments.of(false, true), - Arguments.of(true, false), - Arguments.of(true, true)); - } - @AfterEach void tearDown() throws Exception { try { @@ -115,67 +118,71 @@ void tearDown() throws Exception { } } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testBlockingAggregated(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsGrpcStatusException(() -> client.test(request())); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testBlockingAggregated(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsGrpcStatusException(() -> client.test(newRequest())); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testBlockingRequestStreaming(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsGrpcStatusException(() -> client.testRequestStream(singletonList(request()))); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testBlockingRequestStreaming(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsGrpcStatusException(() -> client.testRequestStream(singletonList(newRequest()))); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testBlockingResponseStreaming(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsGrpcStatusException(() -> client.testResponseStream(request()).forEach(__ -> { /* noop */ })); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testBlockingResponseStreaming(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsGrpcStatusException(() -> client.testResponseStream(newRequest()).forEach(__ -> { /* noop */ })); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testBlockingBiDiStreaming(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsGrpcStatusException(() -> client.testBiDiStream(singletonList(request())) + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testBlockingBiDiStreaming(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsGrpcStatusException(() -> client.testBiDiStream(singletonList(newRequest())) .forEach(__ -> { /* noop */ })); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testAggregated(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsExecutionException(() -> client.asClient().test(request()).toFuture().get()); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testAggregated(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsExecutionException(() -> client.asClient().test(newRequest()).toFuture().get()); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testRequestStreaming(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsExecutionException(() -> client.asClient().testRequestStream(from(request())).toFuture().get()); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testRequestStreaming(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsExecutionException(() -> client.asClient().testRequestStream(from(newRequest())).toFuture().get()); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testResponseStreaming(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsExecutionException(() -> client.asClient().testResponseStream(request()).toFuture().get()); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testResponseStreaming(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsExecutionException(() -> client.asClient().testResponseStream(newRequest()).toFuture().get()); } - @ParameterizedTest(name = "streaming={0} has-trailers={1}") - @MethodSource("params") - void testBiDiStreaming(boolean streaming, boolean hasTrailers) throws Exception { - setUp(streaming, hasTrailers); - assertThrowsExecutionException(() -> client.asClient().testBiDiStream(from(request())).toFuture().get()); + @ParameterizedTest(name = "has-trailers={0}") + @ValueSource(booleans = {true, false}) + void testBiDiStreaming(boolean hasTrailers) throws Exception { + setUp(hasTrailers); + assertThrowsExecutionException(() -> client.asClient().testBiDiStream(from(newRequest())).toFuture().get()); } - private static TestRequest request() { + private static TestRequest newRequest() { return TestRequest.newBuilder().setName("request").build(); } + private static TestResponse newResponse() { + return TestResponse.newBuilder().setMessage("response").build(); + } + private static void assertThrowsExecutionException(Executable executable) { ExecutionException ex = assertThrows(ExecutionException.class, executable); assertThat(ex.getCause(), is(instanceOf(GrpcStatusException.class))); diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientValidatesContentTypeTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientValidatesContentTypeTest.java index a9c76101e9..a74408eba3 100644 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientValidatesContentTypeTest.java +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcClientValidatesContentTypeTest.java @@ -15,141 +15,82 @@ */ package io.servicetalk.grpc.netty; -import io.servicetalk.buffer.api.Buffer; -import io.servicetalk.buffer.api.BufferAllocator; -import io.servicetalk.concurrent.BlockingIterable; import io.servicetalk.concurrent.api.Publisher; -import io.servicetalk.grpc.api.GrpcSerializationProvider; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.grpc.api.GrpcServiceContext; import io.servicetalk.grpc.netty.TesterProto.TestRequest; -import io.servicetalk.grpc.protobuf.ProtoBufSerializationProviderBuilder; -import io.servicetalk.http.api.HttpHeaders; -import io.servicetalk.http.api.HttpPayloadWriter; -import io.servicetalk.http.api.HttpSerializer; -import io.servicetalk.http.api.HttpServerBuilder; -import io.servicetalk.http.api.StatelessTrailersTransformer; +import io.servicetalk.grpc.netty.TesterProto.TestResponse; +import io.servicetalk.http.api.HttpServiceContext; +import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; -import io.servicetalk.http.api.StreamingHttpService; -import io.servicetalk.http.netty.HttpServers; +import io.servicetalk.http.api.StreamingHttpResponseFactory; +import io.servicetalk.http.api.StreamingHttpServiceFilter; import io.servicetalk.transport.api.ServerContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; -import java.util.function.Function; -import java.util.stream.Stream; import javax.annotation.Nullable; import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.encoding.api.Identity.identity; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.noOffloadsStrategy; -import static io.servicetalk.grpc.api.GrpcStatusCode.OK; -import static io.servicetalk.http.api.HttpApiConversions.toHttpService; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; -import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; -import static java.lang.String.valueOf; import static java.util.Collections.singletonList; final class GrpcClientValidatesContentTypeTest { + @Nullable + private ServerContext serverContext; + @Nullable + private TesterProto.Tester.BlockingTesterClient client; - private static final GrpcSerializationProvider SERIALIZATION_PROVIDER = new ProtoBufSerializationProviderBuilder() - .registerMessageType(TestRequest.class, TestRequest.parser()) - .registerMessageType(TesterProto.TestResponse.class, TesterProto.TestResponse.parser()) - .build(); - - private static final Function> - SERIALIZER_OVERRIDING_CONTENT_TYPE = - (withCharset) -> new HttpSerializer() { - - final HttpSerializer delegate = SERIALIZATION_PROVIDER - .serializerFor(identity(), TesterProto.TestResponse.class); - - @Override - public Buffer serialize(final HttpHeaders headers, final TesterProto.TestResponse value, - final BufferAllocator allocator) { - try { - return delegate.serialize(headers, value, allocator); - } finally { - headers.set(CONTENT_TYPE, headers.get(CONTENT_TYPE) + (withCharset ? "; charset=UTF-8" : "")); + void setUp(boolean withCharset) throws Exception { + serverContext = GrpcServers.forAddress(localAddress(0)) + .appendHttpServiceFilter(service -> new StreamingHttpServiceFilter(service) { + @Override + public Single handle( + final HttpServiceContext ctx, final StreamingHttpRequest request, + final StreamingHttpResponseFactory responseFactory) { + return delegate().handle(ctx, request, responseFactory).map(resp -> { + resp.headers().set(CONTENT_TYPE, resp.headers().get(CONTENT_TYPE) + + (withCharset ? "; charset=UTF-8" : "")); + return resp; + }); } - } - - @Override - public BlockingIterable serialize(final HttpHeaders headers, - final BlockingIterable value, - final BufferAllocator allocator) { - try { - return delegate.serialize(headers, value, allocator); - } finally { - headers.set(CONTENT_TYPE, headers.get(CONTENT_TYPE) + (withCharset ? "; charset=UTF-8" : "")); + }) + .listenAndAwait(new TesterProto.Tester.TesterService() { + @Override + public Single testRequestStream(final GrpcServiceContext ctx, + final Publisher request) { + return succeeded(newResponse()); } - } - - @Override - public Publisher serialize(final HttpHeaders headers, - final Publisher value, - final BufferAllocator allocator) { - try { - return delegate.serialize(headers, value, allocator); - } finally { - headers.set(CONTENT_TYPE, headers.get(CONTENT_TYPE) + (withCharset ? "; charset=UTF-8" : "")); - } - } - - @Override - public HttpPayloadWriter serialize( - final HttpHeaders headers, final HttpPayloadWriter payloadWriter, - final BufferAllocator allocator) { - try { - return delegate.serialize(headers, payloadWriter, allocator); - } finally { - headers.set(CONTENT_TYPE, headers.get(CONTENT_TYPE) + (withCharset ? "; charset=UTF-8" : "")); + + @Override + public Publisher testResponseStream(final GrpcServiceContext ctx, + final TestRequest request) { + return from(newResponse()); } - } - }; - @Nullable - private ServerContext serverContext; - @Nullable - private TesterProto.Tester.BlockingTesterClient client; + @Override + public Publisher testBiDiStream(final GrpcServiceContext ctx, + final Publisher request) { + return from(newResponse()); + } - void setUp(boolean streaming, boolean withCharset) throws Exception { - StreamingHttpService streamingService = (ctx, request, responseFactory) -> { - final StreamingHttpResponse response = responseFactory.ok() - .version(request.version()) - .payloadBody(from(TesterProto.TestResponse.newBuilder().setMessage("response").build()), - SERIALIZER_OVERRIDING_CONTENT_TYPE.apply(withCharset)); - - response.transform(new StatelessTrailersTransformer() { - @Override - protected HttpHeaders payloadComplete(final HttpHeaders trailers) { - return trailers.set("grpc-status", valueOf(OK.value())); - } - }); - return succeeded(response); - }; - HttpServerBuilder serverBuilder = HttpServers.forAddress(localAddress(0)) - .protocols(h2Default()); - serverContext = streaming ? serverBuilder.listenStreamingAndAwait(streamingService) : - serverBuilder.listenAndAwait(toHttpService(streamingService)); + @Override + public Single test(final GrpcServiceContext ctx, final TestRequest request) { + return succeeded(newResponse()); + } + }); client = GrpcClients.forAddress(serverHostAndPort(serverContext)) .executionStrategy(noOffloadsStrategy()) .buildBlocking(new TesterProto.Tester.ClientFactory()); } - static Stream params() { - return Stream.of( - Arguments.of(false, false), - Arguments.of(false, true), - Arguments.of(true, false), - Arguments.of(true, true)); - } - @AfterEach void tearDown() throws Exception { try { @@ -159,64 +100,68 @@ void tearDown() throws Exception { } } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testBlockingAggregated(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.test(request()); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testBlockingAggregated(boolean withCharset) throws Exception { + setUp(withCharset); + client.test(newRequest()); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testBlockingRequestStreaming(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.testRequestStream(singletonList(request())); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testBlockingRequestStreaming(boolean withCharset) throws Exception { + setUp(withCharset); + client.testRequestStream(singletonList(newRequest())); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testBlockingResponseStreaming(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.testResponseStream(request()).forEach(__ -> { /* noop */ }); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testBlockingResponseStreaming(boolean withCharset) throws Exception { + setUp(withCharset); + client.testResponseStream(newRequest()).forEach(__ -> { /* noop */ }); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testBlockingBiDiStreaming(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.testBiDiStream(singletonList(request())) + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testBlockingBiDiStreaming(boolean withCharset) throws Exception { + setUp(withCharset); + client.testBiDiStream(singletonList(newRequest())) .forEach(__ -> { /* noop */ }); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testAggregated(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.asClient().test(request()).toFuture().get(); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testAggregated(boolean withCharset) throws Exception { + setUp(withCharset); + client.asClient().test(newRequest()).toFuture().get(); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testRequestStreaming(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.asClient().testRequestStream(from(request())).toFuture().get(); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testRequestStreaming(boolean withCharset) throws Exception { + setUp(withCharset); + client.asClient().testRequestStream(from(newRequest())).toFuture().get(); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testResponseStreaming(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.asClient().testResponseStream(request()).toFuture().get(); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testResponseStreaming(boolean withCharset) throws Exception { + setUp(withCharset); + client.asClient().testResponseStream(newRequest()).toFuture().get(); } - @ParameterizedTest(name = "streaming={0} with-charset={1}") - @MethodSource("params") - void testBiDiStreaming(boolean streaming, boolean withCharset) throws Exception { - setUp(streaming, withCharset); - client.asClient().testBiDiStream(from(request())).toFuture().get(); + @ParameterizedTest(name = "with-charset={0}") + @ValueSource(booleans = {true, false}) + void testBiDiStreaming(boolean withCharset) throws Exception { + setUp(withCharset); + client.asClient().testBiDiStream(from(newRequest())).toFuture().get(); } - private static TestRequest request() { + private static TestRequest newRequest() { return TestRequest.newBuilder().setName("request").build(); } + + private TestResponse newResponse() { + return TestResponse.newBuilder().setMessage("response").build(); + } } diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcMessageEncodingTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcMessageEncodingTest.java deleted file mode 100644 index 5409354660..0000000000 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/GrpcMessageEncodingTest.java +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright © 2020-2021 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.grpc.netty; - -import io.servicetalk.buffer.api.Buffer; -import io.servicetalk.buffer.api.BufferAllocator; -import io.servicetalk.concurrent.api.Publisher; -import io.servicetalk.concurrent.api.Single; -import io.servicetalk.encoding.api.ContentCodec; -import io.servicetalk.grpc.api.GrpcServerBuilder; -import io.servicetalk.grpc.api.GrpcServiceContext; -import io.servicetalk.grpc.api.GrpcStatusCode; -import io.servicetalk.grpc.api.GrpcStatusException; -import io.servicetalk.grpc.netty.TesterProto.TestRequest; -import io.servicetalk.grpc.netty.TesterProto.TestResponse; -import io.servicetalk.grpc.netty.TesterProto.Tester.TestRequestStreamMetadata; -import io.servicetalk.http.api.HttpHeaders; -import io.servicetalk.http.api.HttpServiceContext; -import io.servicetalk.http.api.StreamingHttpRequest; -import io.servicetalk.http.api.StreamingHttpResponse; -import io.servicetalk.http.api.StreamingHttpResponseFactory; -import io.servicetalk.http.api.StreamingHttpService; -import io.servicetalk.http.api.StreamingHttpServiceFilter; -import io.servicetalk.http.api.StreamingHttpServiceFilterFactory; -import io.servicetalk.transport.api.ServerContext; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.function.Executable; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.stream.Stream; -import java.util.zip.DeflaterOutputStream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import java.util.zip.InflaterInputStream; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static io.grpc.internal.GrpcUtil.MESSAGE_ACCEPT_ENCODING; -import static io.grpc.internal.GrpcUtil.MESSAGE_ENCODING; -import static io.servicetalk.buffer.api.Buffer.asInputStream; -import static io.servicetalk.buffer.api.Buffer.asOutputStream; -import static io.servicetalk.buffer.api.CharSequences.contentEquals; -import static io.servicetalk.concurrent.api.Publisher.from; -import static io.servicetalk.concurrent.api.Single.failed; -import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.encoding.api.Identity.identity; -import static io.servicetalk.encoding.api.internal.HeaderUtils.encodingFor; -import static io.servicetalk.encoding.netty.ContentCodings.deflateDefault; -import static io.servicetalk.encoding.netty.ContentCodings.gzipDefault; -import static io.servicetalk.grpc.netty.TesterProto.Tester.ClientFactory; -import static io.servicetalk.grpc.netty.TesterProto.Tester.ServiceFactory; -import static io.servicetalk.grpc.netty.TesterProto.Tester.TestBiDiStreamMetadata; -import static io.servicetalk.grpc.netty.TesterProto.Tester.TestMetadata; -import static io.servicetalk.grpc.netty.TesterProto.Tester.TestResponseStreamMetadata; -import static io.servicetalk.grpc.netty.TesterProto.Tester.TesterClient; -import static io.servicetalk.grpc.netty.TesterProto.Tester.TesterService; -import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; -import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; -import static java.lang.String.valueOf; -import static java.util.Arrays.asList; -import static java.util.Arrays.stream; -import static java.util.Collections.disjoint; -import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.toList; -import static java.util.zip.GZIPInputStream.GZIP_MAGIC; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.internal.util.io.IOUtil.closeQuietly; - -class GrpcMessageEncodingTest { - - private static final int PAYLOAD_SIZE = 512; - private static final ContentCodec CUSTOM_ENCODING = new ContentCodec() { - - private static final int OUGHT_TO_BE_ENOUGH = 1 << 20; - - @Override - public String name() { - return "CUSTOM_ENCODING"; - } - - @Override - public Buffer encode(final Buffer src, final BufferAllocator allocator) { - final Buffer dst = allocator.newBuffer(OUGHT_TO_BE_ENOUGH); - DeflaterOutputStream output = null; - try { - output = new GZIPOutputStream(asOutputStream(dst)); - output.write(src.array(), src.arrayOffset() + src.readerIndex(), src.readableBytes()); - src.skipBytes(src.readableBytes()); - output.finish(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - closeQuietly(output); - } - - return dst; - } - - @Override - public Buffer decode(final Buffer src, final BufferAllocator allocator) { - final Buffer dst = allocator.newBuffer(OUGHT_TO_BE_ENOUGH); - InflaterInputStream input = null; - try { - input = new GZIPInputStream(asInputStream(src)); - - int read = dst.setBytesUntilEndStream(0, input, OUGHT_TO_BE_ENOUGH); - dst.writerIndex(read); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - closeQuietly(input); - } - - return dst; - } - - @Override - public Publisher encode(final Publisher from, final BufferAllocator allocator) { - throw new UnsupportedOperationException(); - } - - @Override - public Publisher decode(final Publisher from, final BufferAllocator allocator) { - throw new UnsupportedOperationException(); - } - - @Override - public String toString() { - return "GrpcMessageEncoding{encoding='CUSTOM_ENCODING'}"; - } - }; - - private static final BiFunction, StreamingHttpServiceFilterFactory> - REQ_RESP_VERIFIER = (options, errors) -> new StreamingHttpServiceFilterFactory() { - @Override - public StreamingHttpServiceFilter create(final StreamingHttpService service) { - return new StreamingHttpServiceFilter(service) { - - @Override - public Single handle(final HttpServiceContext ctx, - final StreamingHttpRequest request, - final StreamingHttpResponseFactory responseFactory) { - final ContentCodec reqEncoding = options.requestEncoding; - final List clientSupportedEncodings = options.clientSupported; - final List serverSupportedEncodings = options.serverSupported; - - try { - request.transformPayloadBody(bufferPublisher -> bufferPublisher.map((buffer -> { - try { - byte compressedFlag = buffer.getByte(0); - - if (reqEncoding == gzipDefault() || reqEncoding.name().equals(CUSTOM_ENCODING.name())) { - int actualHeader = buffer.getShortLE(5) & 0xFFFF; - assertEquals(GZIP_MAGIC, actualHeader); - } - - if (!identity().equals(reqEncoding)) { - assertTrue(buffer.readableBytes() < PAYLOAD_SIZE, - "Compressed content length should be less than the " + - "original payload size"); - } else { - assertTrue(buffer.readableBytes() > PAYLOAD_SIZE, - "Uncompressed content length should be more than the " + - "original payload size"); - } - - assertEquals(!identity().equals(reqEncoding) ? 1 : 0, compressedFlag); - } catch (Throwable t) { - errors.add(t); - throw t; - } - return buffer; - }))); - - assertValidCodingHeader(clientSupportedEncodings, request.headers()); - if (identity().equals(reqEncoding)) { - assertThat("Message-Encoding should NOT be present in the headers if identity", - request.headers().contains(MESSAGE_ENCODING), is(false)); - } else { - assertTrue( - contentEquals(reqEncoding.name(), request.headers().get(MESSAGE_ENCODING)), - "Message-Encoding should be present in the headers if not identity"); - } - } catch (Throwable t) { - errors.add(t); - throw t; - } - - return super.handle(ctx, request, responseFactory).map((response -> { - try { - handle0(clientSupportedEncodings, serverSupportedEncodings, response); - } catch (Throwable t) { - errors.add(t); - throw t; - } - - return response; - })); - } - - private void handle0(final List clientSupportedEncodings, - final List serverSupportedEncodings, - final StreamingHttpResponse response) { - - assertValidCodingHeader(serverSupportedEncodings, response.headers()); - - if (clientSupportedEncodings.isEmpty() || serverSupportedEncodings.isEmpty() || - disjoint(serverSupportedEncodings, clientSupportedEncodings)) { - assertThat("Response shouldn't contain Message-Encoding header if identity", - response.headers().contains(MESSAGE_ENCODING), is(false)); - } else { - ContentCodec expected = identity(); - for (ContentCodec codec : clientSupportedEncodings) { - if (serverSupportedEncodings.contains(codec)) { - expected = codec; - break; - } - } - - if (identity().equals(expected)) { - assertThat("Response shouldn't contain Message-Encoding header if identity", - response.headers().contains(MESSAGE_ENCODING), is(false)); - } else { - assertEquals(expected, encodingFor(clientSupportedEncodings, - response.headers().get(MESSAGE_ENCODING))); - } - } - - response.transformPayloadBody(bufferPublisher -> bufferPublisher.map((buffer -> { - try { - final ContentCodec respEnc = encodingFor(clientSupportedEncodings, - valueOf(response.headers().get(MESSAGE_ENCODING))); - - if (buffer.readableBytes() > 0) { - byte compressedFlag = buffer.getByte(0); - assertEquals(respEnc != null ? 1 : 0, compressedFlag); - - if (respEnc != null && - (respEnc == gzipDefault() || respEnc.name().equals(CUSTOM_ENCODING.name()))) { - int actualHeader = buffer.getShortLE(5) & 0xFFFF; - assertEquals(GZIP_MAGIC, actualHeader); - } - - if (respEnc != null) { - assertTrue(buffer.readableBytes() < PAYLOAD_SIZE, - "Compressed content length should be less than the original " + - "payload size"); - } else { - assertTrue( - buffer.readableBytes() > PAYLOAD_SIZE, - "Uncompressed content length should be more than the original " + - "payload size " + buffer.readableBytes()); - } - } - } catch (Throwable t) { - errors.add(t); - throw t; - } - return buffer; - }))); - } - }; - } - }; - - private static class TesterServiceImpl implements TesterService { - - @Override - public Single test(GrpcServiceContext ctx, TestRequest request) { - return succeeded(TestResponse.newBuilder().setMessage("Reply: " + request.getName()).build()); - } - - @Override - public Single testRequestStream(GrpcServiceContext ctx, Publisher request) { - try { - List requestList = request.collect((Supplier>) ArrayList::new, - (testRequests, testRequest) -> { - testRequests.add(testRequest); - return testRequests; - }).toFuture().get(); - - TestRequest elem = requestList.get(0); - return succeeded(TestResponse.newBuilder().setMessage("Reply: " + elem.getName()).build()); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - - return failed(new IllegalStateException()); - } - - @Override - public Publisher testBiDiStream(GrpcServiceContext ctx, Publisher request) { - return request.map((req) -> TestResponse.newBuilder().setMessage("Reply: " + req.getName()).build()); - } - - @Override - public Publisher testResponseStream(GrpcServiceContext ctx, TestRequest request) { - return from(TestResponse.newBuilder().setMessage("Reply: " + request.getName()).build()); - } - } - - @Nullable - private GrpcServerBuilder grpcServerBuilder; - @Nullable - private ServerContext serverContext; - @Nullable - private TesterClient client; - private final BlockingQueue errors = new LinkedBlockingQueue<>(); - - void setUp(final List serverSupportedCodings, - final List clientSupportedCodings, - final ContentCodec requestEncoding) throws Exception { - - TestEncodingScenario options = new TestEncodingScenario(requestEncoding, clientSupportedCodings, - serverSupportedCodings); - - grpcServerBuilder = GrpcServers.forAddress(localAddress(0)); - serverContext = listenAndAwait(options); - client = newClient(clientSupportedCodings); - } - - static Stream params() { - return Stream.of( - Arguments.of(null, null, identity(), true), - Arguments.of(null, asList(gzipDefault(), identity()), gzipDefault(), false), - Arguments.of(null, asList(deflateDefault(), identity()), deflateDefault(), false), - Arguments.of(asList(gzipDefault(), deflateDefault(), identity()), null, identity(), true), - Arguments.of(asList(identity(), gzipDefault(), deflateDefault()), - asList(gzipDefault(), identity()), gzipDefault(), true), - Arguments.of(asList(identity(), gzipDefault(), deflateDefault()), - asList(deflateDefault(), identity()), deflateDefault(), true), - Arguments.of(asList(identity(), gzipDefault()), asList(deflateDefault(), identity()), - deflateDefault(), false), - Arguments.of(asList(identity(), deflateDefault()), asList(gzipDefault(), identity()), - gzipDefault(), false), - Arguments.of(asList(identity(), deflateDefault()), asList(deflateDefault(), identity()), - deflateDefault(), true), - Arguments.of(asList(identity(), deflateDefault()), null, identity(), true), - Arguments.of(singletonList(gzipDefault()), singletonList(identity()), identity(), true), - Arguments.of(singletonList(gzipDefault()), asList(gzipDefault(), identity()), identity(), true), - Arguments.of(singletonList(gzipDefault()), asList(gzipDefault(), identity()), identity(), true), - Arguments.of(singletonList(gzipDefault()), asList(gzipDefault(), identity()), gzipDefault(), true), - Arguments.of(singletonList(gzipDefault()), asList(deflateDefault(), gzipDefault()), - gzipDefault(), true), - Arguments.of(null, asList(gzipDefault(), identity()), gzipDefault(), false), - Arguments.of(null, asList(gzipDefault(), deflateDefault(), identity()), deflateDefault(), false), - Arguments.of(null, asList(gzipDefault(), identity()), identity(), true), - Arguments.of(singletonList(CUSTOM_ENCODING), singletonList(CUSTOM_ENCODING), CUSTOM_ENCODING, true) - ); - } - - @AfterEach - void tearDown() throws Exception { - try { - client.close(); - } finally { - serverContext.close(); - } - } - - private ServerContext listenAndAwait(final TestEncodingScenario encodingOptions) throws Exception { - - StreamingHttpServiceFilterFactory filterFactory = REQ_RESP_VERIFIER.apply(encodingOptions, errors); - return grpcServerBuilder.appendHttpServiceFilter(filterFactory) - .listenAndAwait(new ServiceFactory(new TesterServiceImpl(), encodingOptions.serverSupported)); - } - - private TesterClient newClient(@Nullable final List supportedCodings) { - return GrpcClients.forAddress(serverHostAndPort(serverContext)) - .build(supportedCodings != null ? - new ClientFactory().supportedMessageCodings(supportedCodings) : - new ClientFactory()); - } - - @ParameterizedTest(name = "server-supported-encodings={0} client-supported-encodings={1} " + - "request-encoding={2} expected-success={3}") - @MethodSource("params") - void test(final List serverSupportedCodings, - final List clientSupportedCodings, - final ContentCodec requestEncoding, - final boolean expectedSuccess) throws Throwable { - setUp(serverSupportedCodings, clientSupportedCodings, requestEncoding); - try { - if (expectedSuccess) { - assertSuccessful(requestEncoding); - } else { - assertUnimplemented(requestEncoding); - } - } catch (Throwable t) { - throwAsyncErrors(); - throw t; - } - } - - private void throwAsyncErrors() throws Throwable { - final Throwable error = errors.poll(); - if (error != null) { - throw error; - } - } - - private static TestRequest request() { - byte[] payload = new byte[PAYLOAD_SIZE]; - Arrays.fill(payload, (byte) 'a'); - String name = new String(payload, StandardCharsets.US_ASCII); - return TestRequest.newBuilder().setName(name).build(); - } - - private void assertSuccessful(final ContentCodec encoding) throws ExecutionException, InterruptedException { - client.test(new TestMetadata(encoding), request()).toFuture().get(); - client.testRequestStream(new TestRequestStreamMetadata(encoding), from(request(), request(), request(), - request(), request())).toFuture().get(); - - client.testResponseStream(new TestResponseStreamMetadata(encoding), request()).forEach(__ -> { /* noop */ }); - client.testBiDiStream(new TestBiDiStreamMetadata(encoding), from(request(), request(), request(), - request(), request())).toFuture().get(); - } - - private void assertUnimplemented(final ContentCodec encoding) { - assertThrowsGrpcStatusUnimplemented(() -> client.test(new TestMetadata(encoding), request()).toFuture().get()); - assertThrowsGrpcStatusUnimplemented(() -> client.testRequestStream(new TestRequestStreamMetadata(encoding), - from(request(), request(), request(), request(), request())).toFuture().get()); - assertThrowsGrpcStatusUnimplemented(() -> client.testResponseStream(new TestResponseStreamMetadata(encoding), - request()).toFuture().get().forEach(__ -> { /* noop */ })); - assertThrowsGrpcStatusUnimplemented(() -> client.testBiDiStream(new TestBiDiStreamMetadata(encoding), - from(request(), request(), request(), request(), request())).toFuture().get()); - } - - private void assertThrowsGrpcStatusUnimplemented(final Executable executable) { - ExecutionException ex = assertThrows(ExecutionException.class, executable); - assertThat(ex.getCause(), is(instanceOf(GrpcStatusException.class))); - assertGrpcStatusException((GrpcStatusException) ex.getCause()); - } - - private static void assertGrpcStatusException(GrpcStatusException grpcStatusException) { - assertThat(grpcStatusException.status().code(), is(GrpcStatusCode.UNIMPLEMENTED)); - } - - private static void assertValidCodingHeader(final List supportedEncodings, - final HttpHeaders headers) { - if (supportedEncodings.size() == 1 && supportedEncodings.contains(identity())) { - assertThat(headers, not(contains(MESSAGE_ACCEPT_ENCODING))); - } else { - final List actualRespAcceptedEncodings = stream(headers - .get(MESSAGE_ACCEPT_ENCODING, "NOT_PRESENT").toString().split(",")) - .map((String::trim)).collect(toList()); - - final List expectedRespAcceptedEncodings = encodingsAsStrings(supportedEncodings); - - if (!expectedRespAcceptedEncodings.isEmpty() && !actualRespAcceptedEncodings.isEmpty()) { - assertEquals(expectedRespAcceptedEncodings, actualRespAcceptedEncodings); - } - } - } - - @Nonnull - private static List encodingsAsStrings(final List supportedEncodings) { - return supportedEncodings.stream() - .filter(enc -> !identity().equals(enc)) - .map(ContentCodec::name) - .map(CharSequence::toString) - .collect(toList()); - } - - static class TestEncodingScenario { - final ContentCodec requestEncoding; - final List clientSupported; - final List serverSupported; - - TestEncodingScenario(final ContentCodec requestEncoding, - @Nullable final List clientSupported, - @Nullable final List serverSupported) { - this.requestEncoding = requestEncoding; - this.clientSupported = clientSupported == null ? singletonList(identity()) : clientSupported; - this.serverSupported = serverSupported == null ? singletonList(identity()) : serverSupported; - } - } -} diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/HttpResponseUponGrpcRequestTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/HttpResponseUponGrpcRequestTest.java index 1a667cc154..6ec8aeee2d 100644 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/HttpResponseUponGrpcRequestTest.java +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/HttpResponseUponGrpcRequestTest.java @@ -28,17 +28,17 @@ import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; final class HttpResponseUponGrpcRequestTest { @@ -49,7 +49,7 @@ final class HttpResponseUponGrpcRequestTest { ServerContext serverContext = HttpServers.forAddress(localAddress(0)) .protocols(h2Default()) .listenAndAwait((ctx, request, responseFactory) -> - succeeded(responseFactory.badRequest().payloadBody(responsePayload, textSerializer()))); + succeeded(responseFactory.badRequest().payloadBody(responsePayload, textSerializerUtf8()))); client = GrpcClients.forAddress(serverHostAndPort(serverContext)) .buildBlocking(new TesterProto.Tester.ClientFactory()); @@ -112,9 +112,8 @@ private static void assertThrowsGrpcStatusException(Executable executable) { private static void assertGrpcStatusException(GrpcStatusException grpcStatusException) { assertThat(grpcStatusException.status().code(), is(GrpcStatusCode.INTERNAL)); - assertThat(grpcStatusException.status().description(), notNullValue()); - assertTrue(grpcStatusException.status().description().contains("status code: 400 Bad Request")); - assertTrue(grpcStatusException.status().description().contains("invalid content-type: text/plain;")); - assertTrue(grpcStatusException.status().description().contains("headers:")); + String description = grpcStatusException.status().description(); + assertThat(description, notNullValue()); + assertThat(description, containsString("invalid content-type: text/plain;")); } } diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java index c941605a0f..2eb4c80aae 100644 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/netty/ProtocolCompatibilityTest.java @@ -21,8 +21,15 @@ import io.servicetalk.concurrent.api.Completable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.encoding.api.ContentCodec; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.encoding.api.EmptyBufferDecoderGroup; +import io.servicetalk.encoding.api.Identity; +import io.servicetalk.encoding.netty.NettyBufferEncoders; +import io.servicetalk.grpc.api.DefaultGrpcClientMetadata; import io.servicetalk.grpc.api.GrpcClientBuilder; +import io.servicetalk.grpc.api.GrpcClientMetadata; import io.servicetalk.grpc.api.GrpcExecutionContext; import io.servicetalk.grpc.api.GrpcExecutionStrategy; import io.servicetalk.grpc.api.GrpcPayloadWriter; @@ -81,6 +88,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -94,8 +102,6 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.api.SourceAdapters.fromSource; import static io.servicetalk.concurrent.internal.TestTimeoutConstants.DEFAULT_TIMEOUT_SECONDS; -import static io.servicetalk.encoding.api.Identity.identity; -import static io.servicetalk.encoding.netty.ContentCodings.gzipDefault; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.defaultStrategy; import static io.servicetalk.grpc.api.GrpcExecutionStrategies.noOffloadsStrategy; import static io.servicetalk.test.resources.DefaultTestCerts.loadServerKey; @@ -620,18 +626,15 @@ private static void testBlockingRequestResponse(final BlockingCompatClient clien final boolean streaming, @Nullable final String compression) throws Exception { try { - final ContentCodec codec = serviceTalkCodingFor(compression); - + final BufferEncoder compressor = serviceTalkCompression(compression); + final GrpcClientMetadata metadata = compressor == null ? DefaultGrpcClientMetadata.INSTANCE : + new DefaultGrpcClientMetadata(compressor); if (!streaming) { - final ScalarCallMetadata metadata = codec == null ? ScalarCallMetadata.INSTANCE : - new ScalarCallMetadata(codec); final CompatResponse response1 = client.scalarCall(metadata, CompatRequest.newBuilder().setId(1).build()); assertEquals(1000001, response1.getSize()); } else { // clientStreamingCall returns the "sum" - final ClientStreamingCallMetadata metadata = codec == null ? ClientStreamingCallMetadata.INSTANCE : - new ClientStreamingCallMetadata(codec); final CompatResponse response2 = client.clientStreamingCall(metadata, asList( CompatRequest.newBuilder().setId(1).build(), CompatRequest.newBuilder().setId(2).build(), @@ -640,10 +643,8 @@ private static void testBlockingRequestResponse(final BlockingCompatClient clien assertEquals(1000006, response2.getSize()); // serverStreamingCall returns a stream from 0 to N-1 - final ServerStreamingCallMetadata serverMetadata = codec == null ? - ServerStreamingCallMetadata.INSTANCE : new ServerStreamingCallMetadata(codec); final BlockingIterable response3 = - client.serverStreamingCall(serverMetadata, CompatRequest.newBuilder().setId(3).build()); + client.serverStreamingCall(metadata, CompatRequest.newBuilder().setId(3).build()); final List response3List = new ArrayList<>(); response3.forEach(response3List::add); assertEquals(3, response3List.size()); @@ -652,9 +653,7 @@ private static void testBlockingRequestResponse(final BlockingCompatClient clien assertEquals(1000002, response3List.get(2).getSize()); // bidirectionalStreamingCall basically echos also - final BidirectionalStreamingCallMetadata bidiMetadata = codec == null ? - BidirectionalStreamingCallMetadata.INSTANCE : new BidirectionalStreamingCallMetadata(codec); - final BlockingIterable response4 = client.bidirectionalStreamingCall(bidiMetadata, + final BlockingIterable response4 = client.bidirectionalStreamingCall(metadata, asList(CompatRequest.newBuilder().setId(3).build(), CompatRequest.newBuilder().setId(4).build(), CompatRequest.newBuilder().setId(5).build() @@ -676,19 +675,17 @@ private static void testRequestResponse(final CompatClient client, final TestSer final boolean streaming, @Nullable final String compression) { try { - final ContentCodec codec = serviceTalkCodingFor(compression); + final BufferEncoder compressor = serviceTalkCompression(compression); + final GrpcClientMetadata metadata = compressor == null ? DefaultGrpcClientMetadata.INSTANCE : + new DefaultGrpcClientMetadata(compressor); if (!streaming) { // scalarCall basically echos - final ScalarCallMetadata metadata = codec == null ? ScalarCallMetadata.INSTANCE : - new ScalarCallMetadata(codec); final Single response1 = client.scalarCall(metadata, CompatRequest.newBuilder().setId(1).build()); assertEquals(1000001, response1.toFuture().get().getSize()); } else { // clientStreamingCall returns the "sum" - final ClientStreamingCallMetadata metadata = codec == null ? ClientStreamingCallMetadata.INSTANCE : - new ClientStreamingCallMetadata(codec); final Single response2 = client.clientStreamingCall(metadata, Publisher.from( CompatRequest.newBuilder().setId(1).build(), CompatRequest.newBuilder().setId(2).build(), @@ -698,10 +695,8 @@ private static void testRequestResponse(final CompatClient client, final TestSer assertEquals(1000006, r.getSize()); // serverStreamingCall returns a stream from 0 to N-1 - final ServerStreamingCallMetadata streamingCallMetadata = codec == null ? - ServerStreamingCallMetadata.INSTANCE : new ServerStreamingCallMetadata(codec); final Publisher response3 = - client.serverStreamingCall(streamingCallMetadata, CompatRequest.newBuilder().setId(3).build()); + client.serverStreamingCall(metadata, CompatRequest.newBuilder().setId(3).build()); final List response3List = new ArrayList<>(response3.toFuture().get()); assertEquals(3, response3List.size()); assertEquals(1000000, response3List.get(0).getSize()); @@ -709,9 +704,7 @@ private static void testRequestResponse(final CompatClient client, final TestSer assertEquals(1000002, response3List.get(2).getSize()); // bidirectionalStreamingCall basically echos also - final BidirectionalStreamingCallMetadata bidiMetadata = codec == null ? - BidirectionalStreamingCallMetadata.INSTANCE : new BidirectionalStreamingCallMetadata(codec); - final Publisher response4 = client.bidirectionalStreamingCall(bidiMetadata, + final Publisher response4 = client.bidirectionalStreamingCall(metadata, Publisher.from(CompatRequest.newBuilder().setId(3).build(), CompatRequest.newBuilder().setId(4).build(), CompatRequest.newBuilder().setId(5).build() @@ -780,11 +773,9 @@ private static void testGrpcErrorStreaming(final CompatClient client, final Test @Nullable final String expectMessage) throws Exception { try { - ContentCodec codec = serviceTalkCodingFor(compression); - BidirectionalStreamingCallMetadata metadata = BidirectionalStreamingCallMetadata.INSTANCE; - if (codec != null) { - metadata = new BidirectionalStreamingCallMetadata(codec); - } + BufferEncoder encoder = serviceTalkCompression(compression); + GrpcClientMetadata metadata = encoder == null ? DefaultGrpcClientMetadata.INSTANCE : + new DefaultGrpcClientMetadata(encoder); final Publisher streamingResponse = client.bidirectionalStreamingCall(metadata, Publisher.from(CompatRequest.newBuilder().setId(3).build(), @@ -804,11 +795,9 @@ private static void testGrpcErrorScalar(final CompatClient client, final TestSer @Nullable final String expectMessage) throws Exception { try { - ContentCodec codec = serviceTalkCodingFor(compression); - ScalarCallMetadata metadata = ScalarCallMetadata.INSTANCE; - if (codec != null) { - metadata = new ScalarCallMetadata(codec); - } + BufferEncoder encoder = serviceTalkCompression(compression); + GrpcClientMetadata metadata = encoder == null ? DefaultGrpcClientMetadata.INSTANCE : + new DefaultGrpcClientMetadata(encoder); final Single scalarResponse = client.scalarCall(metadata, CompatRequest.newBuilder().setId(1).build()); @@ -849,7 +838,7 @@ private static void assertGrpcStatusException(final GrpcStatusException statusEx throws InvalidProtocolBufferException { final GrpcStatus grpcStatus = statusException.status(); assertNotNull(grpcStatus); - assertEquals(expectStatusCode, grpcStatus.code()); + assertEquals(expectStatusCode, grpcStatus.code(), "grpcStatus: " + grpcStatus); if (null != expectMessage) { assertEquals(expectMessage, grpcStatus.description()); } @@ -955,8 +944,8 @@ private static CompatClient serviceTalkClient(final SocketAddress serverAddress, if (null != timeout) { builder.defaultTimeout(timeout); } - List codings = serviceTalkCodingsFor(compression); - return builder.build(new Compat.ClientFactory().supportedMessageCodings(codings)); + return builder.build(new Compat.ClientFactory() + .bufferDecoderGroup(serviceTalkDecompression(compression))); } private static GrpcServerBuilder serviceTalkServerBuilder(final ErrorMode errorMode, @@ -987,85 +976,93 @@ public Single handle(final HttpServiceContext ctx, } private static TestServerContext serviceTalkServerBlocking(final ErrorMode errorMode, final boolean ssl, - @Nullable final String compression) - throws Exception { - - List codings = serviceTalkCodingsFor(compression); - + @Nullable final String compression) throws Exception { final ServerContext serverContext = serviceTalkServerBuilder(ErrorMode.NONE, ssl, null) - .listenAndAwait(new ServiceFactory(new BlockingCompatService() { - @Override - public void bidirectionalStreamingCall(final GrpcServiceContext ctx, - final BlockingIterable request, - final GrpcPayloadWriter responseWriter) - throws Exception { - maybeThrowFromRpc(errorMode); - for (CompatRequest requestItem : request) { - responseWriter.write(computeResponse(requestItem.getId())); - } - responseWriter.close(); - } + .listenAndAwait(new ServiceFactory.Builder() + .bufferDecoderGroup(serviceTalkDecompression(compression)) + .bufferEncoders(serviceTalkCompressions(compression)) + .addService(new BlockingCompatService() { + @Override + public void bidirectionalStreamingCall( + final GrpcServiceContext ctx, final BlockingIterable request, + final GrpcPayloadWriter responseWriter) throws Exception { + maybeThrowFromRpc(errorMode); + for (CompatRequest requestItem : request) { + responseWriter.write(computeResponse(requestItem.getId())); + } + responseWriter.close(); + } - @Override - public CompatResponse clientStreamingCall(final GrpcServiceContext ctx, - final BlockingIterable request) { - maybeThrowFromRpc(errorMode); - int sum = 0; - for (CompatRequest requestItem : request) { - sum += requestItem.getId(); - } - return computeResponse(sum); - } + @Override + public CompatResponse clientStreamingCall(final GrpcServiceContext ctx, + final BlockingIterable request) { + maybeThrowFromRpc(errorMode); + int sum = 0; + for (CompatRequest requestItem : request) { + sum += requestItem.getId(); + } + return computeResponse(sum); + } - @Override - public CompatResponse scalarCall(final GrpcServiceContext ctx, final CompatRequest request) { - maybeThrowFromRpc(errorMode); - return computeResponse(request.getId()); - } + @Override + public CompatResponse scalarCall(final GrpcServiceContext ctx, + final CompatRequest request) { + maybeThrowFromRpc(errorMode); + return computeResponse(request.getId()); + } - @Override - public void serverStreamingCall(final GrpcServiceContext ctx, final CompatRequest request, - final GrpcPayloadWriter responseWriter) - throws Exception { - maybeThrowFromRpc(errorMode); - for (int i = 0; i < request.getId(); i++) { - responseWriter.write(computeResponse(i)); - } - responseWriter.close(); - } - }, codings)); + @Override + public void serverStreamingCall(final GrpcServiceContext ctx, final CompatRequest request, + final GrpcPayloadWriter responseWriter) + throws Exception { + maybeThrowFromRpc(errorMode); + for (int i = 0; i < request.getId(); i++) { + responseWriter.write(computeResponse(i)); + } + responseWriter.close(); + } + }).build()); return TestServerContext.fromServiceTalkServerContext(serverContext); } @Nullable - private static ContentCodec serviceTalkCodingFor(@Nullable final String compression) { + private static BufferEncoder serviceTalkCompression(@Nullable final String compression) { if (compression == null) { return null; } - if (compression.contentEquals(identity().name())) { - return identity(); + if (compression.contentEquals(NettyBufferEncoders.gzipDefault().encodingName())) { + return NettyBufferEncoders.gzipDefault(); + } else if (compression.contentEquals(Identity.identityEncoder().encodingName())) { + return Identity.identityEncoder(); } - - if (compression.contentEquals(gzipDefault().name())) { - return gzipDefault(); - } - throw new UnsupportedOperationException("Unsupported compression " + compression); } - private static List serviceTalkCodingsFor(@Nullable final String compression) { - List codings = new ArrayList<>(); - if (compression != null) { - if (compression.contentEquals(gzipDefault().name())) { - codings.add(gzipDefault()); - } + private static BufferDecoderGroup serviceTalkDecompression(@Nullable final String compression) { + if (compression == null) { + return EmptyBufferDecoderGroup.INSTANCE; + } + BufferDecoderGroupBuilder builder = new BufferDecoderGroupBuilder(2); + if (compression.contentEquals(NettyBufferEncoders.gzipDefault().encodingName())) { + builder.add(NettyBufferEncoders.gzipDefault(), true); + } else if (compression.contentEquals(Identity.identityEncoder().encodingName())) { + builder.add(Identity.identityEncoder(), false); + } + return builder.build(); + } - if (compression.contentEquals(identity().name())) { - codings.add(identity()); - } + private static List serviceTalkCompressions(@Nullable final String compression) { + if (compression == null) { + return Collections.emptyList(); } - return codings; + List encoders = new ArrayList<>(2); + if (compression.contentEquals(NettyBufferEncoders.gzipDefault().encodingName())) { + encoders.add(NettyBufferEncoders.gzipDefault()); + } else if (compression.contentEquals(Identity.identityEncoder().encodingName())) { + encoders.add(Identity.identityEncoder()); + } + return encoders; } private static void maybeThrowFromRpc(final ErrorMode errorMode) { @@ -1133,16 +1130,14 @@ private CompatResponse response(final int value) { } }; - List codings = serviceTalkCodingsFor(compression); - - final ServiceFactory serviceFactory = strategy == defaultStrategy() ? - new ServiceFactory(compatService, codings) : - new ServiceFactory.Builder(codings) - .bidirectionalStreamingCall(strategy, compatService) - .clientStreamingCall(strategy, compatService) - .scalarCall(strategy, compatService) - .serverStreamingCall(strategy, compatService) - .build(); + final ServiceFactory serviceFactory = new ServiceFactory.Builder() + .bufferEncoders(serviceTalkCompressions(compression)) + .bufferDecoderGroup(serviceTalkDecompression(compression)) + .bidirectionalStreamingCall(strategy, compatService) + .clientStreamingCall(strategy, compatService) + .scalarCall(strategy, compatService) + .serverStreamingCall(strategy, compatService) + .build(); serviceFactory.appendServiceFilter(delegate -> new Compat.CompatServiceFilter(delegate) { @Override @@ -1229,6 +1224,13 @@ public Publisher bidirectionalStreamingCall(final Publisher bidirectionalStreamingCall( + final GrpcClientMetadata metadata, final Publisher request) { + return bidirectionalStreamingCall(request); + } + + @Deprecated @Override public Publisher bidirectionalStreamingCall( final BidirectionalStreamingCallMetadata metadata, final Publisher request) { @@ -1245,12 +1247,19 @@ public Single clientStreamingCall(final Publisher return (Single) processor; } + @Deprecated @Override public Single clientStreamingCall(final ClientStreamingCallMetadata metadata, final Publisher request) { return clientStreamingCall(request); } + @Override + public Single clientStreamingCall(final GrpcClientMetadata metadata, + final Publisher request) { + return clientStreamingCall(request); + } + @SuppressWarnings("unchecked") @Override public Single scalarCall(final CompatRequest request) { @@ -1259,11 +1268,17 @@ public Single scalarCall(final CompatRequest request) { return (Single) processor; } + @Deprecated @Override public Single scalarCall(final ScalarCallMetadata metadata, final CompatRequest request) { return scalarCall(request); } + @Override + public Single scalarCall(final GrpcClientMetadata metadata, final CompatRequest request) { + return scalarCall(request); + } + @Override public Publisher serverStreamingCall(final CompatRequest request) { final PublisherSource.Processor processor = @@ -1272,12 +1287,19 @@ public Publisher serverStreamingCall(final CompatRequest request return fromSource(processor); } + @Deprecated @Override public Publisher serverStreamingCall(final ServerStreamingCallMetadata metadata, final CompatRequest request) { return serverStreamingCall(request); } + @Override + public Publisher serverStreamingCall(final GrpcClientMetadata metadata, + final CompatRequest request) { + return serverStreamingCall(request); + } + @Override public void close() throws Exception { channel.shutdown().awaitTermination(DEFAULT_TIMEOUT_SECONDS, SECONDS); @@ -1365,7 +1387,7 @@ private static TestServerContext grpcJavaServer(final ErrorMode errorMode, final } // Always include identity otherwise it's not available - dRegistry = dRegistry.with((Decompressor) id, true); + dRegistry = dRegistry.with((Decompressor) id, false); cRegistry.register(id); builder.decompressorRegistry(dRegistry); diff --git a/servicetalk-grpc-protobuf/build.gradle b/servicetalk-grpc-protobuf/build.gradle index eeced03220..7348c15b2a 100644 --- a/servicetalk-grpc-protobuf/build.gradle +++ b/servicetalk-grpc-protobuf/build.gradle @@ -32,6 +32,9 @@ dependencies { implementation project(":servicetalk-annotations") implementation project(":servicetalk-buffer-netty") + implementation project(":servicetalk-serializer-api") + implementation project(":servicetalk-data-protobuf") + implementation project(":servicetalk-serializer-utils") implementation "org.slf4j:slf4j-api:$slf4jVersion" implementation "com.google.code.findbugs:jsr305:$jsr305Version" diff --git a/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProvider.java b/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProvider.java index 08984b9be6..d7da0d53a0 100644 --- a/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProvider.java +++ b/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProvider.java @@ -44,6 +44,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +@Deprecated final class ProtoBufSerializationProvider implements SerializationProvider { private static final int LENGTH_PREFIXED_MESSAGE_HEADER_BYTES = 5; diff --git a/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProviderBuilder.java b/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProviderBuilder.java index 08e02dda0b..5b307d6073 100644 --- a/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProviderBuilder.java +++ b/servicetalk-grpc-protobuf/src/main/java/io/servicetalk/grpc/protobuf/ProtoBufSerializationProviderBuilder.java @@ -49,9 +49,12 @@ /** * A builder for building a {@link GrpcSerializationProvider} that can serialize and deserialize * pre-registered protocol buffer objects. + * @deprecated The gRPC framing is now built into grpc-netty. This class is no longer necessary and will be removed in + * a future release. * {@link #registerMessageType(Class, Parser)} is used to add one or more {@link MessageLite} message types. Resulting * {@link GrpcSerializationProvider} from {@link #build()} will only serialize and deserialize those message types. */ +@Deprecated public final class ProtoBufSerializationProviderBuilder { private static final CharSequence GRPC_MESSAGE_ENCODING_KEY = newAsciiString("grpc-encoding"); private static final CharSequence APPLICATION_GRPC_PROTO = newAsciiString("application/grpc+proto"); diff --git a/servicetalk-grpc-protoc/build.gradle b/servicetalk-grpc-protoc/build.gradle index 817185f6fc..fe47eeef8b 100644 --- a/servicetalk-grpc-protoc/build.gradle +++ b/servicetalk-grpc-protoc/build.gradle @@ -27,6 +27,10 @@ apply plugin: "com.google.protobuf" dependencies { implementation project(":servicetalk-annotations") + + // runtimeOnly is enough for this module in isolation, but we want transitive modules to include at compile time too. + api project(":servicetalk-data-protobuf") + implementation "com.google.code.findbugs:jsr305:$jsr305Version" implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.squareup:javapoet:$javaPoetVersion" diff --git a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/FileDescriptor.java b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/FileDescriptor.java index 6096781401..5295b3147e 100644 --- a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/FileDescriptor.java +++ b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/FileDescriptor.java @@ -44,7 +44,7 @@ import static javax.lang.model.element.Modifier.STATIC; /** - * A single protoc file for which we will be generating classes + * A single protoc file for which we will be generating classes */ final class FileDescriptor implements GenerationContext { private static final String GENERATED_BY_COMMENT = "Generated by ServiceTalk proto compiler"; diff --git a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java index 763e6cfd2c..372432b50c 100644 --- a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java +++ b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Generator.java @@ -36,6 +36,7 @@ import static com.squareup.javapoet.MethodSpec.constructorBuilder; import static com.squareup.javapoet.MethodSpec.methodBuilder; +import static com.squareup.javapoet.TypeName.BOOLEAN; import static com.squareup.javapoet.TypeSpec.classBuilder; import static com.squareup.javapoet.TypeSpec.interfaceBuilder; import static io.servicetalk.grpc.protoc.Generator.NewRpcMethodFlag.BLOCKING; @@ -57,18 +58,25 @@ import static io.servicetalk.grpc.protoc.Types.BlockingRoute; import static io.servicetalk.grpc.protoc.Types.BlockingStreamingClientCall; import static io.servicetalk.grpc.protoc.Types.BlockingStreamingRoute; +import static io.servicetalk.grpc.protoc.Types.BufferDecoderGroup; +import static io.servicetalk.grpc.protoc.Types.BufferEncoderList; import static io.servicetalk.grpc.protoc.Types.ClientCall; +import static io.servicetalk.grpc.protoc.Types.Collections; import static io.servicetalk.grpc.protoc.Types.Completable; import static io.servicetalk.grpc.protoc.Types.ContentCodec; import static io.servicetalk.grpc.protoc.Types.DefaultGrpcClientMetadata; +import static io.servicetalk.grpc.protoc.Types.EmptyBufferDecoderGroup; import static io.servicetalk.grpc.protoc.Types.FilterableGrpcClient; import static io.servicetalk.grpc.protoc.Types.GrpcBindableService; import static io.servicetalk.grpc.protoc.Types.GrpcClient; import static io.servicetalk.grpc.protoc.Types.GrpcClientCallFactory; import static io.servicetalk.grpc.protoc.Types.GrpcClientFactory; import static io.servicetalk.grpc.protoc.Types.GrpcClientFilterFactory; +import static io.servicetalk.grpc.protoc.Types.GrpcClientMetadata; import static io.servicetalk.grpc.protoc.Types.GrpcExecutionContext; import static io.servicetalk.grpc.protoc.Types.GrpcExecutionStrategy; +import static io.servicetalk.grpc.protoc.Types.GrpcMethodDescriptor; +import static io.servicetalk.grpc.protoc.Types.GrpcMethodDescriptors; import static io.servicetalk.grpc.protoc.Types.GrpcPayloadWriter; import static io.servicetalk.grpc.protoc.Types.GrpcRouteExecutionStrategyFactory; import static io.servicetalk.grpc.protoc.Types.GrpcRoutes; @@ -79,7 +87,10 @@ import static io.servicetalk.grpc.protoc.Types.GrpcServiceFilterFactory; import static io.servicetalk.grpc.protoc.Types.GrpcStatusException; import static io.servicetalk.grpc.protoc.Types.GrpcSupportedCodings; +import static io.servicetalk.grpc.protoc.Types.Identity; +import static io.servicetalk.grpc.protoc.Types.Objects; import static io.servicetalk.grpc.protoc.Types.ProtoBufSerializationProviderBuilder; +import static io.servicetalk.grpc.protoc.Types.ProtobufSerializerFactory; import static io.servicetalk.grpc.protoc.Types.Publisher; import static io.servicetalk.grpc.protoc.Types.RequestStreamingClientCall; import static io.servicetalk.grpc.protoc.Types.RequestStreamingRoute; @@ -98,17 +109,23 @@ import static io.servicetalk.grpc.protoc.Words.Factory; import static io.servicetalk.grpc.protoc.Words.Filter; import static io.servicetalk.grpc.protoc.Words.INSTANCE; +import static io.servicetalk.grpc.protoc.Words.JAVADOC_DEPRECATED; import static io.servicetalk.grpc.protoc.Words.JAVADOC_PARAM; import static io.servicetalk.grpc.protoc.Words.JAVADOC_RETURN; import static io.servicetalk.grpc.protoc.Words.JAVADOC_THROWS; import static io.servicetalk.grpc.protoc.Words.Metadata; +import static io.servicetalk.grpc.protoc.Words.PROTOBUF; +import static io.servicetalk.grpc.protoc.Words.PROTO_CONTENT_TYPE; import static io.servicetalk.grpc.protoc.Words.RPC_PATH; import static io.servicetalk.grpc.protoc.Words.Rpc; import static io.servicetalk.grpc.protoc.Words.Service; import static io.servicetalk.grpc.protoc.Words.To; +import static io.servicetalk.grpc.protoc.Words.addService; import static io.servicetalk.grpc.protoc.Words.append; import static io.servicetalk.grpc.protoc.Words.appendServiceFilter; import static io.servicetalk.grpc.protoc.Words.bind; +import static io.servicetalk.grpc.protoc.Words.bufferDecoderGroup; +import static io.servicetalk.grpc.protoc.Words.bufferEncoders; import static io.servicetalk.grpc.protoc.Words.builder; import static io.servicetalk.grpc.protoc.Words.client; import static io.servicetalk.grpc.protoc.Words.close; @@ -122,8 +139,11 @@ import static io.servicetalk.grpc.protoc.Words.existing; import static io.servicetalk.grpc.protoc.Words.factory; import static io.servicetalk.grpc.protoc.Words.initSerializationProvider; +import static io.servicetalk.grpc.protoc.Words.isSupportedMessageCodingsEmpty; import static io.servicetalk.grpc.protoc.Words.metadata; +import static io.servicetalk.grpc.protoc.Words.methodDescriptor; import static io.servicetalk.grpc.protoc.Words.onClose; +import static io.servicetalk.grpc.protoc.Words.registerRoutes; import static io.servicetalk.grpc.protoc.Words.request; import static io.servicetalk.grpc.protoc.Words.requestEncoding; import static io.servicetalk.grpc.protoc.Words.responseWriter; @@ -293,78 +313,92 @@ private TypeSpec.Builder addSerializationProviderInit(final State state, .build() ); + serviceClassBuilder.addMethod(methodBuilder(isSupportedMessageCodingsEmpty) + .addModifiers(PRIVATE, STATIC) + .returns(BOOLEAN) + .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) + .addStatement("return $L.isEmpty() || ($L.size() == 1 && $T.identity().equals($L.get(0)))", + supportedMessageCodings, supportedMessageCodings, Identity, supportedMessageCodings) + .build()); + return serviceClassBuilder; } - private TypeSpec.Builder addServiceRpcInterfaces(final State state, final TypeSpec.Builder serviceClassBuilder) { - List methodDescriptorProtoList = state.serviceProto.getMethodList(); - for (int i = 0; i < methodDescriptorProtoList.size(); ++i) { - final int methodIndex = i; - MethodDescriptorProto methodProto = methodDescriptorProtoList.get(i); - final String name = context.deconflictJavaTypeName( - sanitizeIdentifier(methodProto.getName(), false) + Rpc); - - final FieldSpec.Builder pathSpecBuilder = FieldSpec.builder(String.class, RPC_PATH) - .addModifiers(PUBLIC, STATIC, FINAL) // redundant, default for interface field - .initializer("$S", context.methodPath(state.serviceProto, methodProto)); - final TypeSpec.Builder interfaceSpecBuilder = interfaceBuilder(name) - .addAnnotation(FunctionalInterface.class) - .addModifiers(PUBLIC) - .addField(pathSpecBuilder.build()) - .addMethod(newRpcMethodSpec(methodProto, EnumSet.of(INTERFACE), printJavaDocs, - (__, b) -> { - b.addModifiers(ABSTRACT).addParameter(GrpcServiceContext, ctx); - if (printJavaDocs) { - extractJavaDocComments(state, methodIndex, b); - b.addJavadoc(JAVADOC_PARAM + ctx + " context associated with this service and request." + - lineSeparator()); - } - return b; - })) - .addSuperinterface(GrpcService); - - if (methodProto.hasOptions() && methodProto.getOptions().getDeprecated()) { - interfaceSpecBuilder.addAnnotation(Deprecated.class); - } + private static FieldSpec newMethodDescriptorSpec( + final ClassName inClass, final ClassName outClass, final String javaMethodName, + final boolean clientStreaming, final boolean serverStreaming, final String methodHttpPath, + final ParameterizedTypeName methodDescriptorType, final String methodDescFieldName, final boolean isAsync) { + return FieldSpec.builder(methodDescriptorType, methodDescFieldName) + .addModifiers(PRIVATE, STATIC, FINAL) + .initializer("$T.newMethodDescriptor($S, $S, $L, $L, $T.class, $S, " + + "$T.$L.serializerDeserializer($T.parser()), $T::getSerializedSize, $L, $L, " + + "$T.class, $S, $T.$L.serializerDeserializer($T.parser()), $T::getSerializedSize)", + GrpcMethodDescriptors, methodHttpPath, javaMethodName, + clientStreaming, isAsync, inClass, PROTO_CONTENT_TYPE, + ProtobufSerializerFactory, PROTOBUF, inClass, inClass, + serverStreaming, isAsync, outClass, PROTO_CONTENT_TYPE, + ProtobufSerializerFactory, PROTOBUF, outClass, outClass).build(); + } - final TypeSpec interfaceSpec = interfaceSpecBuilder.build(); - state.serviceRpcInterfaces.add(new RpcInterface(methodProto, false, ClassName.bestGuess(name))); - serviceClassBuilder.addType(interfaceSpec); + private void addServiceRpcInterfaceSpec(final State state, + final TypeSpec.Builder serviceClassBuilder, + final MethodDescriptorProto methodProto, + final int methodIndex, final boolean isAsync) { + final String name = context.deconflictJavaTypeName(isAsync ? + sanitizeIdentifier(methodProto.getName(), false) + Rpc : + Blocking + sanitizeIdentifier(methodProto.getName(), false) + Rpc); + final String methodDescFieldName = name + "_MD"; + final ClassName inClass = messageTypesMap.get(methodProto.getInputType()); + final ClassName outClass = messageTypesMap.get(methodProto.getOutputType()); + final ParameterizedTypeName methodDescriptorType = + ParameterizedTypeName.get(GrpcMethodDescriptor, inClass, outClass); + final String methodHttpPath = context.methodPath(state.serviceProto, methodProto); + final String javaMethodName = routeName(methodProto); + serviceClassBuilder.addField(newMethodDescriptorSpec(inClass, outClass, javaMethodName, + methodProto.getClientStreaming(), methodProto.getServerStreaming(), methodHttpPath, + methodDescriptorType, methodDescFieldName, true)); + + final FieldSpec.Builder pathSpecBuilder = FieldSpec.builder(String.class, RPC_PATH) + .addJavadoc(JAVADOC_DEPRECATED + " Use {@link #$L}." + lineSeparator(), methodDescriptor) + .addAnnotation(Deprecated.class) + .addModifiers(PUBLIC, STATIC, FINAL) // redundant, default for interface field + .initializer("$S", context.methodPath(state.serviceProto, methodProto)); + final TypeSpec.Builder interfaceSpecBuilder = interfaceBuilder(name) + .addAnnotation(FunctionalInterface.class) + .addModifiers(PUBLIC) + .addField(pathSpecBuilder.build()) + .addMethod(methodBuilder(methodDescriptor) + .addModifiers(PUBLIC, STATIC) + .returns(methodDescriptorType) + .addStatement("return $L", methodDescFieldName).build()) + .addMethod(newRpcMethodSpec(inClass, outClass, javaMethodName, methodProto.getClientStreaming(), + methodProto.getServerStreaming(), + isAsync ? EnumSet.of(INTERFACE) : EnumSet.of(INTERFACE, BLOCKING), + printJavaDocs, (__, b) -> { + b.addModifiers(ABSTRACT).addParameter(GrpcServiceContext, ctx); + if (printJavaDocs) { + extractJavaDocComments(state, methodIndex, b); + b.addJavadoc(JAVADOC_PARAM + ctx + + " context associated with this service and request." + lineSeparator()); + } + return b; + })) + .addSuperinterface(isAsync ? GrpcService : BlockingGrpcService); + + if (methodProto.hasOptions() && methodProto.getOptions().getDeprecated()) { + interfaceSpecBuilder.addAnnotation(Deprecated.class); } - List asyncRpcInterfaces = new ArrayList<>(state.serviceRpcInterfaces); - for (int i = 0; i < asyncRpcInterfaces.size(); ++i) { - final int methodIndex = i; - RpcInterface rpcInterface = asyncRpcInterfaces.get(i); - MethodDescriptorProto methodProto = rpcInterface.methodProto; - final String name = context.deconflictJavaTypeName(Blocking + rpcInterface.className.simpleName()); - - final FieldSpec.Builder pathSpecBuilder = FieldSpec.builder(String.class, RPC_PATH) - .addModifiers(PUBLIC, STATIC, FINAL) // redundant, default for interface fields - .initializer("$T.$L", rpcInterface.className, RPC_PATH); - final TypeSpec.Builder interfaceSpecBuilder = interfaceBuilder(name) - .addAnnotation(FunctionalInterface.class) - .addModifiers(PUBLIC) - .addField(pathSpecBuilder.build()) - .addMethod(newRpcMethodSpec(methodProto, EnumSet.of(BLOCKING, INTERFACE), printJavaDocs, - (__, b) -> { - b.addModifiers(ABSTRACT).addParameter(GrpcServiceContext, ctx); - if (printJavaDocs) { - extractJavaDocComments(state, methodIndex, b); - b.addJavadoc(JAVADOC_PARAM + ctx + " context associated with this service and request." + - lineSeparator()); - } - return b; - })) - .addSuperinterface(BlockingGrpcService); - - if (methodProto.hasOptions() && methodProto.getOptions().getDeprecated()) { - interfaceSpecBuilder.addAnnotation(Deprecated.class); - } + state.serviceRpcInterfaces.add(new RpcInterface(methodProto, !isAsync, ClassName.bestGuess(name))); + serviceClassBuilder.addType(interfaceSpecBuilder.build()); + } - final TypeSpec interfaceSpec = interfaceSpecBuilder.build(); - state.serviceRpcInterfaces.add(new RpcInterface(methodProto, true, ClassName.bestGuess(name))); - serviceClassBuilder.addType(interfaceSpec); + private TypeSpec.Builder addServiceRpcInterfaces(final State state, final TypeSpec.Builder serviceClassBuilder) { + List methodDescriptorProtoList = state.serviceProto.getMethodList(); + for (int i = 0; i < methodDescriptorProtoList.size(); ++i) { + MethodDescriptorProto methodProto = methodDescriptorProtoList.get(i); + addServiceRpcInterfaceSpec(state, serviceClassBuilder, methodProto, i, true); + addServiceRpcInterfaceSpec(state, serviceClassBuilder, methodProto, i, false); } return serviceClassBuilder; @@ -390,13 +424,9 @@ private void extractJavaDocComments(State state, int methodIndex, MethodSpec.Bui * @return the service class builder */ private TypeSpec.Builder addServiceInterfaces(final State state, final TypeSpec.Builder serviceClassBuilder) { - TypeSpec interfaceSpec = newServiceInterfaceSpec(state, false); - serviceClassBuilder.addType(interfaceSpec); - - interfaceSpec = newServiceInterfaceSpec(state, true); - serviceClassBuilder.addType(interfaceSpec); - - return serviceClassBuilder; + return serviceClassBuilder + .addType(newServiceInterfaceSpec(state, false)) + .addType(newServiceInterfaceSpec(state, true)); } private TypeSpec.Builder addServiceFilter(final State state, final TypeSpec.Builder serviceClassBuilder) { @@ -441,17 +471,24 @@ private TypeSpec.Builder addServiceFactory(final State state, final TypeSpec.Bui final TypeSpec.Builder serviceBuilderSpecBuilder = classBuilder(Builder) .addModifiers(PUBLIC, STATIC, FINAL) .addField(FieldSpec.builder(GrpcSupportedCodings, supportedMessageCodings) - .addModifiers(PRIVATE, FINAL) - .build() - ) + .addModifiers(PRIVATE, FINAL).build()) + .addField(FieldSpec.builder(BufferDecoderGroup, bufferDecoderGroup) + .addModifiers(PRIVATE) + .initializer("$T.INSTANCE", EmptyBufferDecoderGroup).build()) + .addField(FieldSpec.builder(BufferEncoderList, bufferEncoders) + .addModifiers(PRIVATE) + .initializer("$T.emptyList()", Collections).build()) .superclass(ParameterizedTypeName.get(GrpcRoutes, state.serviceClass)) .addType(newServiceFromRoutesClassSpec(serviceFromRoutesClass, state.serviceRpcInterfaces, state.serviceClass)) .addMethod(constructorBuilder() .addModifiers(PUBLIC) - .addStatement("this(java.util.Collections.emptyList())") + .addStatement("this($T.emptyList())", Collections) .build()) .addMethod(constructorBuilder() + .addJavadoc(JAVADOC_DEPRECATED + " Use {@link #$L($T)} and {@link #$L($T)}." + lineSeparator(), + bufferDecoderGroup, BufferDecoderGroup, bufferEncoders, BufferEncoderList) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC) .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) .addStatement("this.$L = $L", supportedMessageCodings, supportedMessageCodings) @@ -459,15 +496,33 @@ private TypeSpec.Builder addServiceFactory(final State state, final TypeSpec.Bui .addMethod(constructorBuilder() .addModifiers(PUBLIC) .addParameter(GrpcRouteExecutionStrategyFactory, strategyFactory, FINAL) - .addStatement("this($L, java.util.Collections.emptyList())", strategyFactory) + .addStatement("this($L, $T.emptyList())", strategyFactory, Collections) .build()) .addMethod(constructorBuilder() + .addJavadoc(JAVADOC_DEPRECATED + + " Use {@link #$L($T)}, {@link #$L($T)}, and {@link #$L($T)}." + lineSeparator(), + Builder, GrpcRouteExecutionStrategyFactory, bufferDecoderGroup, BufferDecoderGroup, + bufferEncoders, BufferEncoderList) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC) .addParameter(GrpcRouteExecutionStrategyFactory, strategyFactory, FINAL) .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) .addStatement("super($L)", strategyFactory) .addStatement("this.$L = $L", supportedMessageCodings, supportedMessageCodings) .build()) + .addMethod(methodBuilder(bufferDecoderGroup) + .addModifiers(PUBLIC) + .addParameter(BufferDecoderGroup, bufferDecoderGroup, FINAL) + .returns(builderClass) + .addStatement("this.$L = $T.requireNonNull($L)", bufferDecoderGroup, Objects, + bufferDecoderGroup) + .addStatement("return this").build()) + .addMethod(methodBuilder(bufferEncoders) + .addModifiers(PUBLIC) + .addParameter(BufferEncoderList, bufferEncoders, FINAL) + .returns(builderClass) + .addStatement("this.$L = $T.requireNonNull($L)", bufferEncoders, Objects, bufferEncoders) + .addStatement("return this").build()) .addMethod(methodBuilder("build") .addModifiers(PUBLIC) .returns(state.serviceFactoryClass) @@ -489,15 +544,35 @@ private TypeSpec.Builder addServiceFactory(final State state, final TypeSpec.Bui final String addRouteMethodName = addRouteMethodName(rpcInterface.methodProto, rpcInterface.blocking); final ClassName routeInterfaceClass = routeInterfaceClass(rpcInterface.methodProto, rpcInterface.blocking); + CodeBlock addRouteCode = CodeBlock.builder() + .beginControlFlow("if ($L.isEmpty())", supportedMessageCodings) + .addStatement("$L($L.getClass(), $T.$L(), $L, $L, $L.wrap($L::$L, $L))", addRouteMethodName, rpc, + rpcInterface.className, methodDescriptor, bufferDecoderGroup, bufferEncoders, + routeInterfaceClass, rpc, routeName, rpc) + .nextControlFlow("else") + .addStatement("$L($T.$L, $L.getClass(), $S, $L.wrap($L::$L, $L), $T.class, $T.class, " + + "$L($L))", addRouteMethodName, rpcInterface.className, RPC_PATH, rpc, + routeName, routeInterfaceClass, rpc, routeName, rpc, inClass, outClass, + initSerializationProvider, supportedMessageCodings) + .endControlFlow().build(); + + CodeBlock addRouteExecCode = CodeBlock.builder() + .beginControlFlow("if ($L.isEmpty())", supportedMessageCodings) + .addStatement("$L($L, $T.$L(), $L, $L, $L.wrap($L::$L, $L))", addRouteMethodName, strategy, + rpcInterface.className, methodDescriptor, bufferDecoderGroup, bufferEncoders, + routeInterfaceClass, rpc, routeName, rpc) + .nextControlFlow("else") + .addStatement("$L($T.$L, $L, $L.wrap($L::$L, $L), $T.class, $T.class, $L($L))", + addRouteMethodName, rpcInterface.className, RPC_PATH, strategy, routeInterfaceClass, + rpc, routeName, rpc, inClass, outClass, initSerializationProvider, supportedMessageCodings) + .endControlFlow().build(); + serviceBuilderSpecBuilder .addMethod(methodBuilder(methodName) .addModifiers(PUBLIC) .addParameter(rpcInterface.className, rpc, FINAL) .returns(builderClass) - .addStatement("$L($T.$L, $L.getClass(), $S, $L.wrap($L::$L, $L), $T.class, $T.class, " + - "$L($L))", addRouteMethodName, rpcInterface.className, RPC_PATH, rpc, - routeName, routeInterfaceClass, rpc, routeName, rpc, inClass, outClass, - initSerializationProvider, supportedMessageCodings) + .addCode(addRouteCode) .addStatement("return this") .build()) .addMethod(methodBuilder(methodName) @@ -505,24 +580,39 @@ private TypeSpec.Builder addServiceFactory(final State state, final TypeSpec.Bui .addParameter(GrpcExecutionStrategy, strategy, FINAL) .addParameter(rpcInterface.className, rpc, FINAL) .returns(builderClass) - .addStatement("$L($T.$L, $L, $L.wrap($L::$L, $L), $T.class, $T.class, $L($L))", - addRouteMethodName, rpcInterface.className, RPC_PATH, strategy, routeInterfaceClass, - rpc, routeName, rpc, inClass, outClass, initSerializationProvider, - supportedMessageCodings) + .addCode(addRouteExecCode) .addStatement("return this") .build()); }); - final MethodSpec.Builder registerRoutesMethodSpecBuilder = methodBuilder("registerRoutes") + serviceBuilderSpecBuilder.addMethod(methodBuilder(addService) + .addModifiers(PUBLIC) + .returns(builderClass) + .addParameter(state.serviceClass, service, FINAL) + .addStatement("$L($L)", registerRoutes, service) + .addStatement("return this") + .build()); + + final MethodSpec.Builder addBlockingServiceMethodSpecBuilder = methodBuilder(addService) + .addModifiers(PUBLIC) + .returns(builderClass) + .addParameter(state.blockingServiceClass, service, FINAL); + final MethodSpec.Builder registerRoutesMethodSpecBuilder = methodBuilder(registerRoutes) .addModifiers(PROTECTED) .addAnnotation(Override.class) .addParameter(state.serviceClass, service, FINAL); state.serviceProto.getMethodList().stream() .map(Generator::routeName) - .forEach(n -> registerRoutesMethodSpecBuilder.addStatement("$L($L)", n, service)); + .forEach(n -> { + registerRoutesMethodSpecBuilder.addStatement("$L($L)", n, service); + addBlockingServiceMethodSpecBuilder.addStatement("$L$L($L)", n, Blocking, service); + }); + + addBlockingServiceMethodSpecBuilder.addStatement("return this"); final TypeSpec serviceBuilderType = serviceBuilderSpecBuilder + .addMethod(addBlockingServiceMethodSpecBuilder.build()) .addMethod(registerRoutesMethodSpecBuilder.build()) .build(); @@ -539,6 +629,11 @@ private TypeSpec.Builder addServiceFactory(final State state, final TypeSpec.Bui serviceFactoryBuilderInitChain(state.serviceProto, false)) .build()) .addMethod(constructorBuilder() + .addJavadoc(JAVADOC_DEPRECATED + + " Use {@link $L#$L()}, {@link $L#$L($T)}, and {@link $L#$L($T)}." + lineSeparator(), + Builder, Builder, Builder, bufferDecoderGroup, BufferDecoderGroup, + Builder, bufferEncoders, BufferEncoderList) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC) .addParameter(state.serviceClass, service, FINAL) .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) @@ -553,6 +648,11 @@ private TypeSpec.Builder addServiceFactory(final State state, final TypeSpec.Bui serviceFactoryBuilderInitChain(state.serviceProto, false)) .build()) .addMethod(constructorBuilder() + .addJavadoc(JAVADOC_DEPRECATED + + " Use {@link $L#$L($T)}, {@link $L#$L($T)}, and {@link $L#$L($T)}." + lineSeparator(), + Builder, Builder, GrpcRouteExecutionStrategyFactory, Builder, bufferDecoderGroup, + BufferDecoderGroup, Builder, bufferEncoders, BufferEncoderList) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC) .addParameter(state.serviceClass, service, FINAL) .addParameter(GrpcRouteExecutionStrategyFactory, strategyFactory, FINAL) @@ -567,6 +667,11 @@ supportedMessageCodings, serviceFactoryBuilderInitChain(state.serviceProto, fals serviceFactoryBuilderInitChain(state.serviceProto, true)) .build()) .addMethod(constructorBuilder() + .addJavadoc(JAVADOC_DEPRECATED + + " Use {@link $L#$L()}, {@link $L#$L($T)}, and {@link $L#$L($T)}." + lineSeparator(), + Builder, Builder, Builder, bufferDecoderGroup, BufferDecoderGroup, + Builder, bufferEncoders, BufferEncoderList) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC) .addParameter(state.blockingServiceClass, service, FINAL) .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) @@ -581,6 +686,11 @@ supportedMessageCodings, serviceFactoryBuilderInitChain(state.serviceProto, fals serviceFactoryBuilderInitChain(state.serviceProto, true)) .build()) .addMethod(constructorBuilder() + .addJavadoc(JAVADOC_DEPRECATED + + " Use {@link $L#$L($T)}, {@link $L#$L($T)}, and {@link $L#$L($T)}." + lineSeparator(), + Builder, Builder, GrpcRouteExecutionStrategyFactory, Builder, bufferDecoderGroup, + BufferDecoderGroup, Builder, bufferEncoders, BufferEncoderList) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC) .addParameter(state.blockingServiceClass, service, FINAL) .addParameter(GrpcRouteExecutionStrategyFactory, strategyFactory, FINAL) @@ -625,12 +735,25 @@ private TypeSpec.Builder addClientMetadata(final State state, final TypeSpec.Bui final ClassName metaDataClassName = ClassName.bestGuess(name); final TypeSpec classSpec = classBuilder(name) + .addJavadoc(JAVADOC_DEPRECATED + " This class will be removed in the future in favor of direct " + + "usage of {@link $T}. Deprecation of {@link $T#path()} renders this type unnecessary." + + lineSeparator(), GrpcClientMetadata, GrpcClientMetadata) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC, STATIC, FINAL) .superclass(DefaultGrpcClientMetadata) .addField(FieldSpec.builder(metaDataClassName, INSTANCE) + .addJavadoc(JAVADOC_DEPRECATED + + " This class will be removed in the future in favor of direct usage of {@link $T}." + + lineSeparator(), GrpcClientMetadata) + .addAnnotation(Deprecated.class) .addModifiers(PUBLIC, STATIC, FINAL) // redundant, default for interface field .initializer("new $T()", metaDataClassName) .build()) + .addMethod(constructorBuilder() + .addModifiers(PRIVATE) + .addParameter(GrpcClientMetadata, metadata, FINAL) + .addStatement("super($T.$L, $L)", rpcInterface.className, RPC_PATH, metadata) + .build()) .addMethod(constructorBuilder() .addModifiers(PRIVATE) .addStatement("super($T.$L)", rpcInterface.className, RPC_PATH) @@ -704,8 +827,14 @@ private TypeSpec.Builder addClientInterfaces(final State state, final TypeSpec.B filterableClientSpecBuilder .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(INTERFACE, CLIENT), - printJavaDocs, (__, b) -> { - b.addModifiers(ABSTRACT).addParameter(clientMetaData.className, metadata); + printJavaDocs, (methodName, b) -> { + ClassName inClass = messageTypesMap.get(clientMetaData.methodProto.getInputType()); + b.addModifiers(ABSTRACT).addParameter(clientMetaData.className, metadata) + .addAnnotation(Deprecated.class) + .addJavadoc(JAVADOC_DEPRECATED + " Use {@link #$L($T,$T)}." + lineSeparator(), + methodName, + GrpcClientMetadata, clientMetaData.methodProto.getClientStreaming() ? + ParameterizedTypeName.get(Publisher, inClass) : inClass); if (printJavaDocs) { extractJavaDocComments(state, methodIndex, b); b.addJavadoc(JAVADOC_PARAM + metadata + @@ -714,6 +843,19 @@ private TypeSpec.Builder addClientInterfaces(final State state, final TypeSpec.B return b; })); + filterableClientSpecBuilder + .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(INTERFACE, CLIENT), + printJavaDocs, (methodName, b) -> { + b.addModifiers(DEFAULT).addParameter(GrpcClientMetadata, metadata); + if (printJavaDocs) { + extractJavaDocComments(state, methodIndex, b); + b.addJavadoc(JAVADOC_PARAM + metadata + + " the metadata associated with this client call." + lineSeparator()); + } + return b.addStatement("return $L(new $T($L), $L)", methodName, clientMetaData.className, + metadata, request); + })); + blockingClientSpecBuilder .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(BLOCKING, INTERFACE, CLIENT), printJavaDocs, (__, b) -> { @@ -724,14 +866,30 @@ private TypeSpec.Builder addClientInterfaces(final State state, final TypeSpec.B return b; })) .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(BLOCKING, INTERFACE, CLIENT), - printJavaDocs, (__, b) -> { - b.addModifiers(ABSTRACT).addParameter(clientMetaData.className, metadata); + printJavaDocs, (methodName, b) -> { + ClassName inClass = messageTypesMap.get(clientMetaData.methodProto.getInputType()); + b.addModifiers(ABSTRACT).addParameter(clientMetaData.className, metadata) + .addAnnotation(Deprecated.class) + .addJavadoc(JAVADOC_DEPRECATED + " Use {@link #$L($T,$T)}." + lineSeparator(), + methodName, GrpcClientMetadata, clientMetaData.methodProto.getClientStreaming() ? + ParameterizedTypeName.get(ClassName.get(Iterable.class), inClass) : inClass); if (printJavaDocs) { extractJavaDocComments(state, methodIndex, b); b.addJavadoc(JAVADOC_PARAM + metadata + " the metadata associated with this client call." + lineSeparator()); } return b; + })) + .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(BLOCKING, INTERFACE, CLIENT), + printJavaDocs, (methodName, b) -> { + b.addModifiers(DEFAULT).addParameter(GrpcClientMetadata, metadata); + if (printJavaDocs) { + extractJavaDocComments(state, methodIndex, b); + b.addJavadoc(JAVADOC_PARAM + metadata + + " the metadata associated with this client call." + lineSeparator()); + } + return b.addStatement("return $L(new $T($L), $L)", methodName, clientMetaData.className, + metadata, request); })); } @@ -748,11 +906,18 @@ private TypeSpec.Builder addClientFilter(final State state, final TypeSpec.Build .addMethod(newDelegatingCompletableMethodSpec(onClose, delegate)) .addMethod(newDelegatingMethodSpec(executionContext, delegate, GrpcExecutionContext, null)); + state.clientMetaDatas.forEach(clientMetaData -> + classSpecBuilder.addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(INTERFACE, CLIENT), + false, (n, b) -> b.addAnnotation(Deprecated.class) + .addAnnotation(Override.class) + .addParameter(clientMetaData.className, metadata) + .addStatement("return $L.$L($L, $L)", delegate, n, metadata, request)))); + state.clientMetaDatas.forEach(clientMetaData -> classSpecBuilder.addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(INTERFACE, CLIENT), false, (n, b) -> b.addAnnotation(Override.class) - .addParameter(clientMetaData.className, metadata) - .addStatement("return $L.$L($L, $L)", delegate, n, metadata, request)))); + .addParameter(GrpcClientMetadata, metadata) + .addStatement("return $L.$L($L, $L)", delegate, n, metadata, request)))); serviceClassBuilder.addType(classSpecBuilder.build()); @@ -781,7 +946,7 @@ private TypeSpec.Builder addClientFactory(final State state, final TypeSpec.Buil state.blockingClientClass.simpleName()); final TypeSpec.Builder clientFactorySpecBuilder = classBuilder(clientFactoryClass) - .addModifiers(PUBLIC, STATIC, FINAL) + .addModifiers(PUBLIC, STATIC) .superclass(ParameterizedTypeName.get(GrpcClientFactory, state.clientClass, state.blockingClientClass, state.clientFilterClass, state.filterableClientClass, state.clientFilterFactoryClass)) .addMethod(methodBuilder("appendClientFilterFactory") @@ -797,7 +962,8 @@ private TypeSpec.Builder addClientFactory(final State state, final TypeSpec.Buil .addAnnotation(Override.class) .returns(state.clientClass) .addParameter(GrpcClientCallFactory, factory, FINAL) - .addStatement("return new $T($L, $L())", defaultClientClass, factory, supportedMessageCodings) + .addStatement("return new $T($L, $L(), $L())", defaultClientClass, factory, + supportedMessageCodings, bufferDecoderGroup) .build()) .addMethod(methodBuilder("newFilter") .addModifiers(PROTECTED) @@ -819,8 +985,8 @@ private TypeSpec.Builder addClientFactory(final State state, final TypeSpec.Buil .addAnnotation(Override.class) .returns(state.blockingClientClass) .addParameter(GrpcClientCallFactory, factory, FINAL) - .addStatement("return new $T($L, $L())", defaultBlockingClientClass, factory, - supportedMessageCodings) + .addStatement("return new $T($L, $L(), $L())", defaultBlockingClientClass, factory, + supportedMessageCodings, bufferDecoderGroup) .build()) .addType(newDefaultClientClassSpec(state, defaultClientClass, defaultBlockingClientClass)) .addType(newFilterableClientToClientClassSpec(state, filterableClientToClientClass, @@ -877,23 +1043,27 @@ enum NewRpcMethodFlag { BLOCKING, INTERFACE, CLIENT } - private MethodSpec newRpcMethodSpec(final MethodDescriptorProto methodProto, final EnumSet flags, - final boolean printJavaDocs, - final BiFunction - methodBuilderCustomizer) { - - final ClassName inClass = messageTypesMap.get(methodProto.getInputType()); - final ClassName outClass = messageTypesMap.get(methodProto.getOutputType()); - - final String name = routeName(methodProto); + private MethodSpec newRpcMethodSpec( + final MethodDescriptorProto methodProto, final EnumSet flags, final boolean printJavaDocs, + final BiFunction methodBuilderCustomizer) { + return newRpcMethodSpec(messageTypesMap.get(methodProto.getInputType()), + messageTypesMap.get(methodProto.getOutputType()), routeName(methodProto), + methodProto.getClientStreaming(), methodProto.getServerStreaming(), flags, printJavaDocs, + methodBuilderCustomizer); + } - final MethodSpec.Builder methodSpecBuilder = methodBuilderCustomizer.apply(name, methodBuilder(name)) - .addModifiers(PUBLIC); + private static MethodSpec newRpcMethodSpec( + final ClassName inClass, final ClassName outClass, final String methodName, + final boolean clientSteaming, final boolean serverStreaming, + final EnumSet flags, final boolean printJavaDocs, + final BiFunction methodBuilderCustomizer) { + final MethodSpec.Builder methodSpecBuilder = methodBuilderCustomizer.apply(methodName, + methodBuilder(methodName)).addModifiers(PUBLIC); final Modifier[] mods = flags.contains(INTERFACE) ? new Modifier[0] : new Modifier[]{FINAL}; if (flags.contains(BLOCKING)) { - if (methodProto.getClientStreaming()) { + if (clientSteaming) { if (flags.contains(CLIENT)) { methodSpecBuilder.addParameter(ParameterizedTypeName.get(ClassName.get(Iterable.class), inClass), request, mods); @@ -916,7 +1086,7 @@ private MethodSpec newRpcMethodSpec(final MethodDescriptorProto methodProto, fin } } - if (methodProto.getServerStreaming()) { + if (serverStreaming) { if (flags.contains(CLIENT)) { methodSpecBuilder.returns(ParameterizedTypeName.get(BlockingIterable, outClass)); if (printJavaDocs) { @@ -929,7 +1099,7 @@ private MethodSpec newRpcMethodSpec(final MethodDescriptorProto methodProto, fin responseWriter, mods); if (printJavaDocs) { methodSpecBuilder.addJavadoc(JAVADOC_PARAM + responseWriter + - " used to write a stream of type {@link $T} to the client." + lineSeparator() + + " used to write a stream of type {@link $T} to the server." + lineSeparator() + "The implementation of this method is responsible for calling {@link $T#close()}." + lineSeparator(), outClass, GrpcPayloadWriter); } @@ -951,11 +1121,11 @@ private MethodSpec newRpcMethodSpec(final MethodDescriptorProto methodProto, fin "propagated to the peer.", GrpcStatusException); } } else { - if (methodProto.getClientStreaming()) { + if (clientSteaming) { methodSpecBuilder.addParameter(ParameterizedTypeName.get(Publisher, inClass), request, mods); if (printJavaDocs) { methodSpecBuilder.addJavadoc(JAVADOC_PARAM + request + - " used to read a stream of type {@link $T} from the client." + lineSeparator(), inClass); + " used to write a stream of type {@link $T} to the server." + lineSeparator(), inClass); } } else { methodSpecBuilder.addParameter(inClass, request, mods); @@ -967,7 +1137,7 @@ private MethodSpec newRpcMethodSpec(final MethodDescriptorProto methodProto, fin } } - if (methodProto.getServerStreaming()) { + if (serverStreaming) { methodSpecBuilder.returns(ParameterizedTypeName.get(Publisher, outClass)); if (printJavaDocs) { methodSpecBuilder.addJavadoc(JAVADOC_RETURN + (flags.contains(CLIENT) ? @@ -996,12 +1166,14 @@ private TypeSpec newDefaultBlockingClientClassSpec(final State state, final Clas .addSuperinterface(state.blockingClientClass) .addField(GrpcClientCallFactory, factory, PRIVATE, FINAL) .addField(GrpcSupportedCodings, supportedMessageCodings, PRIVATE, FINAL) + .addField(BufferDecoderGroup, bufferDecoderGroup, PRIVATE, FINAL) .addMethod(methodBuilder("asClient") .addModifiers(PUBLIC) .addAnnotation(Override.class) .returns(state.clientClass) // TODO: Cache client - .addStatement("return new $T($L, $L)", defaultClientClass, factory, supportedMessageCodings) + .addStatement("return new $T($L, $L, $L)", defaultClientClass, factory, supportedMessageCodings, + bufferDecoderGroup) .build()) .addMethod(newDelegatingMethodSpec(executionContext, factory, GrpcExecutionContext, null)) .addMethod(newDelegatingCompletableToBlockingMethodSpec(close, closeAsync, factory)) @@ -1012,8 +1184,10 @@ private TypeSpec newDefaultBlockingClientClassSpec(final State state, final Clas .addModifiers(PRIVATE) .addParameter(GrpcClientCallFactory, factory, FINAL) .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) + .addParameter(BufferDecoderGroup, bufferDecoderGroup, FINAL) .addStatement("this.$N = $N", factory, factory) - .addStatement("this.$N = $N", supportedMessageCodings, supportedMessageCodings); + .addStatement("this.$N = $N", supportedMessageCodings, supportedMessageCodings) + .addStatement("this.$N = $N", bufferDecoderGroup, bufferDecoderGroup); addClientFieldsAndMethods(state, typeSpecBuilder, constructorBuilder, true); @@ -1028,13 +1202,14 @@ private TypeSpec newDefaultClientClassSpec(final State state, final ClassName de .addSuperinterface(state.clientClass) .addField(GrpcClientCallFactory, factory, PRIVATE, FINAL) .addField(GrpcSupportedCodings, supportedMessageCodings, PRIVATE, FINAL) + .addField(BufferDecoderGroup, bufferDecoderGroup, PRIVATE, FINAL) .addMethod(methodBuilder("asBlockingClient") .addModifiers(PUBLIC) .addAnnotation(Override.class) .returns(state.blockingClientClass) // TODO: Cache client - .addStatement("return new $T($L, $L)", defaultBlockingClientClass, - factory, supportedMessageCodings) + .addStatement("return new $T($L, $L, $L)", defaultBlockingClientClass, + factory, supportedMessageCodings, bufferDecoderGroup) .build()) .addMethod(newDelegatingMethodSpec(executionContext, factory, GrpcExecutionContext, null)) .addMethod(newDelegatingCompletableMethodSpec(onClose, factory)) @@ -1048,8 +1223,10 @@ private TypeSpec newDefaultClientClassSpec(final State state, final ClassName de .addModifiers(PRIVATE) .addParameter(GrpcClientCallFactory, factory, FINAL) .addParameter(GrpcSupportedCodings, supportedMessageCodings, FINAL) + .addParameter(BufferDecoderGroup, bufferDecoderGroup, FINAL) .addStatement("this.$N = $N", factory, factory) - .addStatement("this.$N = $N", supportedMessageCodings, supportedMessageCodings); + .addStatement("this.$N = $N", supportedMessageCodings, supportedMessageCodings) + .addStatement("this.$N = $N", bufferDecoderGroup, bufferDecoderGroup); addClientFieldsAndMethods(state, typeSpecBuilder, constructorBuilder, false); @@ -1064,7 +1241,13 @@ private void addClientFieldsAndMethods(final State state, final TypeSpec.Builder final EnumSet rpcMethodSpecsFlags = blocking ? EnumSet.of(BLOCKING, CLIENT) : EnumSet.of(CLIENT); - state.clientMetaDatas.forEach(clientMetaData -> { + assert state.clientMetaDatas.size() == state.serviceRpcInterfaces.size() >>> 1; + for (int i = 0; i < state.clientMetaDatas.size(); ++i) { + ClientMetaData clientMetaData = state.clientMetaDatas.get(i); + // clientMetaDatas only contains async methods, serviceRpcInterfaces include async methods then blocking + // methods. + RpcInterface rpcInterface = state.serviceRpcInterfaces.get((i << 1) + (blocking ? 1 : 0)); + assert blocking == rpcInterface.blocking; final ClassName inClass = messageTypesMap.get(clientMetaData.methodProto.getInputType()); final ClassName outClass = messageTypesMap.get(clientMetaData.methodProto.getOutputType()); final String routeName = routeName(clientMetaData.methodProto); @@ -1075,18 +1258,31 @@ private void addClientFieldsAndMethods(final State state, final TypeSpec.Builder inClass, outClass), callFieldName, PRIVATE, FINAL) .addMethod(newRpcMethodSpec(clientMetaData.methodProto, rpcMethodSpecsFlags, false, (n, b) -> b.addAnnotation(Override.class) - .addStatement("return $L($T.$L, $L)", n, clientMetaData.className, INSTANCE, - request))) + .addStatement("return $L($L.isEmpty() ? $T.$L : $T.$L, $L)", n, + supportedMessageCodings, DefaultGrpcClientMetadata, INSTANCE, + clientMetaData.className, INSTANCE, request))) .addMethod(newRpcMethodSpec(clientMetaData.methodProto, rpcMethodSpecsFlags, false, - (__, b) -> b.addAnnotation(Override.class) + (__, b) -> b.addAnnotation(Deprecated.class) + .addAnnotation(Override.class) .addParameter(clientMetaData.className, metadata, FINAL) + .addStatement("return $L.$L($L, $L)", callFieldName, request, metadata, request))) + .addMethod(newRpcMethodSpec(clientMetaData.methodProto, rpcMethodSpecsFlags, false, + (__, b) -> b.addAnnotation(Override.class) + .addParameter(GrpcClientMetadata, metadata, FINAL) .addStatement("return $L.$L($L, $L)", callFieldName, request, metadata, request))); constructorBuilder - .addStatement("$L = $N.$L($L($L), $T.class, $T.class)", callFieldName, factory, - newCallMethodName(clientMetaData.methodProto, blocking), initSerializationProvider, - supportedMessageCodings, inClass, outClass); - }); + .addCode(CodeBlock.builder() + .beginControlFlow("if ($L.isEmpty())", supportedMessageCodings) + .addStatement("$L = $N.$L($T.$L(), $L)", callFieldName, factory, + newCallMethodName(clientMetaData.methodProto, blocking), rpcInterface.className, + methodDescriptor, bufferDecoderGroup) + .nextControlFlow("else") + .addStatement("$L = $N.$L($L($L), $T.class, $T.class)", callFieldName, factory, + newCallMethodName(clientMetaData.methodProto, blocking), initSerializationProvider, + supportedMessageCodings, inClass, outClass) + .endControlFlow().build()); + } } private TypeSpec newFilterableClientToClientClassSpec(final State state, @@ -1115,11 +1311,21 @@ private TypeSpec newFilterableClientToClientClassSpec(final State state, state.clientMetaDatas.forEach(clientMetaData -> typeSpecBuilder .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(CLIENT), false, + // Keep using clientMetaData.className until the class is removed, after it is removed switch to + // DefaultGrpcClientMetadata.INSTANCE. This is because we don't know if the underlying + // ClientCall object was generated with the MethodDescriptor or not, if it wasn't then we need + // to grab the path() from the metadata. (n, b) -> b.addAnnotation(Override.class) - .addStatement("return $L($T.$L, $L)", n, clientMetaData.className, INSTANCE, request))) + .addStatement("return $L($T.$L, $L)", n, clientMetaData.className, INSTANCE, + request))) .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(CLIENT), false, - (n, b) -> b.addAnnotation(Override.class) + (n, b) -> b.addAnnotation(Deprecated.class) + .addAnnotation(Override.class) .addParameter(clientMetaData.className, metadata, FINAL) + .addStatement("return $L.$L($L, $L)", client, n, metadata, request))) + .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(CLIENT), false, + (n, b) -> b.addAnnotation(Override.class) + .addParameter(GrpcClientMetadata, metadata, FINAL) .addStatement("return $L.$L($L, $L)", client, n, metadata, request)))); return typeSpecBuilder.build(); @@ -1158,8 +1364,14 @@ private TypeSpec newClientToBlockingClientClassSpec(final State state, .addStatement("return $L.$L($L)$L", client, n, requestExpression, responseConversionExpression))) .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(BLOCKING, CLIENT), false, - (n, b) -> b.addAnnotation(Override.class) + (n, b) -> b.addAnnotation(Deprecated.class) + .addAnnotation(Override.class) .addParameter(clientMetaData.className, metadata, FINAL) + .addStatement("return $L.$L($L, $L)$L", client, n, metadata, requestExpression, + responseConversionExpression))) + .addMethod(newRpcMethodSpec(clientMetaData.methodProto, EnumSet.of(BLOCKING, CLIENT), false, + (n, b) -> b.addAnnotation(Override.class) + .addParameter(GrpcClientMetadata, metadata, FINAL) .addStatement("return $L.$L($L, $L)$L", client, n, metadata, requestExpression, responseConversionExpression))); }); diff --git a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Types.java b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Types.java index e079eacc5a..6495b2ba21 100644 --- a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Types.java +++ b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Types.java @@ -32,8 +32,11 @@ final class Types { private static final String grpcRoutesFqcn = grpcApiPkg + ".GrpcRoutes"; private static final String grpcProtobufPkg = grpcBasePkg + ".protobuf"; private static final String routerApiPkg = basePkg + ".router.api"; + private static final String protobufDataPkg = basePkg + ".data.protobuf"; static final ClassName List = ClassName.get("java.util", "List"); + static final ClassName Objects = ClassName.get("java.util", "Objects"); + static final ClassName Collections = ClassName.get("java.util", "Collections"); private static final ClassName RouteExecutionStrategyFactory = bestGuess(routerApiPkg + ".RouteExecutionStrategyFactory"); @@ -47,6 +50,7 @@ final class Types { static final ClassName BlockingGrpcClient = bestGuess(grpcApiPkg + ".BlockingGrpcClient"); static final ClassName BlockingGrpcService = bestGuess(grpcApiPkg + ".BlockingGrpcService"); + static final ClassName GrpcClientMetadata = bestGuess(grpcApiPkg + ".GrpcClientMetadata"); static final ClassName DefaultGrpcClientMetadata = bestGuess(grpcApiPkg + ".DefaultGrpcClientMetadata"); static final ClassName GrpcClient = bestGuess(grpcApiPkg + ".GrpcClient"); static final ClassName GrpcClientCallFactory = bestGuess(grpcApiPkg + ".GrpcClientCallFactory"); @@ -56,6 +60,11 @@ final class Types { static final ClassName GrpcExecutionContext = bestGuess(grpcApiPkg + ".GrpcExecutionContext"); static final ClassName GrpcExecutionStrategy = bestGuess(grpcApiPkg + ".GrpcExecutionStrategy"); static final ClassName GrpcStatusException = bestGuess(grpcApiPkg + ".GrpcStatusException"); + static final ClassName Identity = bestGuess(encodingApiPkg + ".Identity"); + static final ClassName BufferDecoderGroup = bestGuess(encodingApiPkg + ".BufferDecoderGroup"); + static final ClassName EmptyBufferDecoderGroup = bestGuess(encodingApiPkg + ".EmptyBufferDecoderGroup"); + static final ClassName BufferEncoder = bestGuess(encodingApiPkg + ".BufferEncoder"); + static final TypeName BufferEncoderList = ParameterizedTypeName.get(List, BufferEncoder); static final ClassName ContentCodec = bestGuess(encodingApiPkg + ".ContentCodec"); static final TypeName GrpcSupportedCodings = ParameterizedTypeName.get(List, ContentCodec); static final ClassName GrpcPayloadWriter = bestGuess(grpcApiPkg + ".GrpcPayloadWriter"); @@ -66,6 +75,8 @@ final class Types { static final ClassName GrpcServiceContext = bestGuess(grpcApiPkg + ".GrpcServiceContext"); static final ClassName GrpcServiceFactory = bestGuess(grpcApiPkg + ".GrpcServiceFactory"); static final ClassName GrpcServiceFilterFactory = bestGuess(grpcApiPkg + ".GrpcServiceFilterFactory"); + static final ClassName GrpcMethodDescriptor = bestGuess(grpcApiPkg + ".MethodDescriptor"); + static final ClassName GrpcMethodDescriptors = bestGuess(grpcApiPkg + ".MethodDescriptors"); static final ClassName BlockingClientCall = bestGuess(GrpcClientCallFactory + ".BlockingClientCall"); static final ClassName BlockingRequestStreamingClientCall = @@ -92,8 +103,10 @@ final class Types { static final ClassName BlockingRoute = bestGuess(grpcRoutesFqcn + ".BlockingRoute"); static final ClassName BlockingStreamingRoute = bestGuess(grpcRoutesFqcn + ".BlockingStreamingRoute"); + @Deprecated static final ClassName ProtoBufSerializationProviderBuilder = bestGuess(grpcProtobufPkg + ".ProtoBufSerializationProviderBuilder"); + static final ClassName ProtobufSerializerFactory = bestGuess(protobufDataPkg + ".ProtobufSerializerFactory"); static final TypeName GrpcRouteExecutionStrategyFactory = ParameterizedTypeName.get(RouteExecutionStrategyFactory, GrpcExecutionStrategy); diff --git a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Words.java b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Words.java index 4cd4b0c3c7..eaa54765ca 100644 --- a/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Words.java +++ b/servicetalk-grpc-protoc/src/main/java/io/servicetalk/grpc/protoc/Words.java @@ -38,12 +38,18 @@ final class Words { static final String routes = "routes"; static final String rpc = "rpc"; static final String initSerializationProvider = "initSerializationProvider"; + static final String addService = "addService"; + static final String registerRoutes = "registerRoutes"; static final String service = "service"; static final String strategy = "strategy"; static final String requestEncoding = "requestEncoding"; static final String timeout = "timeout"; static final String supportedMessageCodings = "supportedMessageCodings"; + static final String isSupportedMessageCodingsEmpty = "isSupportedMessageCodingsEmpty"; + static final String bufferDecoderGroup = "bufferDecoderGroup"; + static final String bufferEncoders = "bufferEncoders"; static final String strategyFactory = strategy + "Factory"; + static final String methodDescriptor = "methodDescriptor"; static final String Service = "Service"; static final String Blocking = "Blocking"; static final String Builder = "Builder"; @@ -54,13 +60,16 @@ final class Words { static final String Filter = "Filter"; static final String Rpc = "Rpc"; static final String To = "To"; + static final String PROTO_CONTENT_TYPE = "+proto"; static final String INSTANCE = "INSTANCE"; + static final String PROTOBUF = "PROTOBUF"; static final String RPC_PATH = "PATH"; static final String COMMENT_PRE_TAG = "

";
     static final String COMMENT_POST_TAG = "
"; static final String JAVADOC_PARAM = "@param "; static final String JAVADOC_RETURN = "@return "; static final String JAVADOC_THROWS = "@throws "; + static final String JAVADOC_DEPRECATED = "@deprecated"; private Words() { // no instance diff --git a/servicetalk-http-api/build.gradle b/servicetalk-http-api/build.gradle index 4dbff01683..d280f7f67a 100644 --- a/servicetalk-http-api/build.gradle +++ b/servicetalk-http-api/build.gradle @@ -22,6 +22,7 @@ dependencies { api project(":servicetalk-concurrent-api") api project(":servicetalk-logging-api") api project(":servicetalk-serialization-api") + api project(":servicetalk-serializer-api") api project(":servicetalk-transport-api") api project(":servicetalk-oio-api") api project(":servicetalk-encoding-api") @@ -31,6 +32,7 @@ dependencies { implementation project(":servicetalk-concurrent-internal") implementation project(":servicetalk-concurrent-api-internal") implementation project(":servicetalk-encoding-api-internal") + implementation project(":servicetalk-serializer-utils") implementation project(":servicetalk-utils-internal") implementation project(":servicetalk-oio-api-internal") implementation "com.google.code.findbugs:jsr305:$jsr305Version" diff --git a/servicetalk-http-api/gradle/spotbugs/main-exclusions.xml b/servicetalk-http-api/gradle/spotbugs/main-exclusions.xml index 1d9cf4310e..8211a31c6f 100644 --- a/servicetalk-http-api/gradle/spotbugs/main-exclusions.xml +++ b/servicetalk-http-api/gradle/spotbugs/main-exclusions.xml @@ -40,7 +40,9 @@ - + + + diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpRequest.java index bf102dd00d..6dcccb7c69 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpRequest.java @@ -15,6 +15,7 @@ */ package io.servicetalk.http.api; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import io.servicetalk.transport.api.HostAndPort; @@ -89,11 +90,17 @@ public HttpHeaders headers() { return original.headers(); } + @Deprecated @Override public ContentCodec encoding() { return original.encoding(); } + @Override + public BufferEncoder contentEncoding() { + return original.contentEncoding(); + } + @Override public String toString() { return original.toString(); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpResponse.java index 48ecc21e10..f49b30ccb4 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractDelegatingHttpResponse.java @@ -30,6 +30,7 @@ public HttpProtocolVersion version() { return original.version(); } + @Deprecated @Override public ContentCodec encoding() { return original.encoding(); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java index c163b1a0fe..d844b2963a 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java @@ -25,6 +25,7 @@ * Abstract base class for {@link HttpMetaData}. */ abstract class AbstractHttpMetaData implements HttpMetaData { + @Deprecated @Nullable private ContentCodec encoding; private HttpProtocolVersion version; @@ -35,17 +36,6 @@ abstract class AbstractHttpMetaData implements HttpMetaData { this.headers = requireNonNull(headers); } - AbstractHttpMetaData(final HttpProtocolVersion version, final HttpHeaders headers, - @Nullable final ContentCodec encoding) { - this.version = requireNonNull(version); - this.headers = requireNonNull(headers); - this.encoding = encoding; - } - - AbstractHttpMetaData(final AbstractHttpMetaData metaData) { - this(metaData.version, metaData.headers, metaData.encoding); - } - @Override public final HttpProtocolVersion version() { return version; @@ -57,12 +47,14 @@ public HttpMetaData version(final HttpProtocolVersion version) { return this; } + @Deprecated @Override public HttpMetaData encoding(final ContentCodec encoding) { this.encoding = requireNonNull(encoding); return this; } + @Deprecated @Override public ContentCodec encoding() { return encoding; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpMessageBodyUtils.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpMessageBodyUtils.java new file mode 100644 index 0000000000..d1ceb51711 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpMessageBodyUtils.java @@ -0,0 +1,165 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.BlockingIterator; + +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nullable; + +import static java.lang.System.nanoTime; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +final class BlockingStreamingHttpMessageBodyUtils { + private BlockingStreamingHttpMessageBodyUtils() { + } + + static HttpMessageBodyIterable newMessageBody(BlockingIterable rawMsgBody) { + return () -> new DefaultHttpMessageBodyIterator<>(rawMsgBody.iterator()); + } + + static HttpMessageBodyIterable newMessageBody( + BlockingIterable rawMsgBody, HttpHeaders headers, HttpStreamingDeserializer deserializer, + BufferAllocator allocator) { + return () -> new HttpMessageBodyIterator() { + private final HttpMessageBodyIterator itr = + new DefaultHttpMessageBodyIterator<>(rawMsgBody.iterator()); + private final BlockingIterator deserialized = + deserializer.deserialize(headers, () -> itr, allocator).iterator(); + @Nullable + @Override + public HttpHeaders trailers() { + return itr.trailers(); + } + + @Override + public boolean hasNext(final long timeout, final TimeUnit unit) throws TimeoutException { + return deserialized.hasNext(timeout, unit); + } + + @Nullable + @Override + public T next(final long timeout, final TimeUnit unit) throws TimeoutException { + return deserialized.next(timeout, unit); + } + + @Nullable + @Override + public T next() { + return deserialized.next(); + } + + @Override + public void close() throws Exception { + deserialized.close(); + } + + @Override + public boolean hasNext() { + return deserialized.hasNext(); + } + }; + } + + private static final class DefaultHttpMessageBodyIterator implements HttpMessageBodyIterator { + private final BlockingIterator rawMessageBody; + @Nullable + private HttpHeaders trailers; + @Nullable + private Buffer next; + + DefaultHttpMessageBodyIterator(BlockingIterator rawMessageBody) { + this.rawMessageBody = requireNonNull(rawMessageBody); + } + + @Nullable + public HttpHeaders trailers() { + return trailers; + } + + @Override + public boolean hasNext(final long timeout, final TimeUnit unit) throws TimeoutException { + if (next != null) { + return true; + } + long remainingTimeoutNanos = unit.toNanos(timeout); + final long timeStampANanos = nanoTime(); + if (rawMessageBody.hasNext(remainingTimeoutNanos, NANOSECONDS)) { + remainingTimeoutNanos -= nanoTime() - timeStampANanos; + setNext(rawMessageBody.next(remainingTimeoutNanos, NANOSECONDS)); + } + return next != null; + } + + @Override + public Buffer next(final long timeout, final TimeUnit unit) { + return next(); + } + + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + if (rawMessageBody.hasNext()) { + setNext(rawMessageBody.next()); + } + return next != null; + } + + @Override + public Buffer next() { + if (next == null) { + throw new NoSuchElementException(); + } + Buffer tmp = next; + next = null; + return tmp; + } + + @Override + public void remove() { + rawMessageBody.remove(); + } + + @Override + public void close() throws Exception { + rawMessageBody.close(); + } + + private void setNext(@Nullable Object rawNext) { + if (rawNext instanceof Buffer) { + next = (Buffer) rawNext; + } else if (rawNext instanceof HttpHeaders) { + trailers = (HttpHeaders) rawNext; + } else if (rawNext != null) { + try { + close(); + } catch (Exception e) { + throw new IllegalStateException( + "exception while closing due to unsupported type: " + rawNext, e); + } + throw new IllegalArgumentException("unsupported type: " + rawNext); + } + } + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpRequest.java index de82a90462..c23324c284 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpRequest.java @@ -21,6 +21,7 @@ import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.internal.CloseableIteratorBufferAsInputStream; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.io.InputStream; @@ -49,14 +50,38 @@ default InputStream payloadBodyInputStream() { /** * Gets and deserializes the payload body. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)}. * @param deserializer The function that deserializes the underlying {@link BlockingIterable}. * @param The resulting type of the deserialization operation. * @return The results of the deserialization operation. */ + @Deprecated default BlockingIterable payloadBody(HttpDeserializer deserializer) { return deserializer.deserialize(headers(), payloadBody()); } + /** + * Gets and deserializes the payload body. + * @param deserializer The function that deserializes the underlying {@link BlockingIterable}. + * @param The resulting type of the deserialization operation. + * @return The results of the deserialization operation. + */ + BlockingIterable payloadBody(HttpStreamingDeserializer deserializer); + + /** + * Get the {@link HttpMessageBodyIterable} for this request. + * @return the {@link HttpMessageBodyIterable} for this request. + */ + HttpMessageBodyIterable messageBody(); + + /** + * Get the {@link HttpMessageBodyIterable} for this request and deserialize to type {@link T}. + * @param deserializer The function that deserializes the underlying {@link BlockingIterable}. + * @param The resulting type of the deserialization operation. + * @return the {@link HttpMessageBodyIterable} for this request and deserialize to type {@link T}. + */ + HttpMessageBodyIterable messageBody(HttpStreamingDeserializer deserializer); + /** * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload set to {@code payloadBody}. *

@@ -99,6 +124,13 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { */ BlockingStreamingHttpRequest payloadBody(InputStream payloadBody); + /** + * Set the {@link HttpMessageBodyIterable} for this response. + * @param messageBody The new message body. + * @return {@code this}. + */ + BlockingStreamingHttpRequest messageBody(HttpMessageBodyIterable messageBody); + /** * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload set to the result of serialization. *

@@ -108,19 +140,20 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { *

* This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the * combination with the existing payload body that is being replaced. + * @deprecated Use {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param payloadBody The new payload body, prior to serialization. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated BlockingStreamingHttpRequest payloadBody(Iterable payloadBody, HttpSerializer serializer); /** * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload set to the result of serialization. *

* A best effort will be made to apply back pressure to the existing payload body which is being replaced. If this - * default policy is not sufficient you can use {@link #transformPayloadBody(Function, HttpSerializer)} for more - * fine grain control. + * default policy is not sufficient {@link #payloadBody()} can be used to drain with more fine grain control. *

* This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the * combination with the existing payload body that is being replaced. @@ -129,8 +162,35 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { * @param The type of objects to serialize. * @return {@code this} */ + BlockingStreamingHttpRequest payloadBody(Iterable payloadBody, HttpStreamingSerializer serializer); + + /** + * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload set to the result of serialization. + *

+ * A best effort will be made to apply back pressure to the existing payload body which is being replaced. If this + * default policy is not sufficient {@link #payloadBody()} can be used to drain with more fine grain control. + *

+ * This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the + * combination with the existing payload body that is being replaced. + * @deprecated Use {@link #payloadBody(Iterable, HttpStreamingSerializer)}. + * @param payloadBody The new payload body, prior to serialization. + * @param serializer Used to serialize the payload body. + * @param The type of objects to serialize. + * @return {@code this} + */ + @Deprecated BlockingStreamingHttpRequest payloadBody(CloseableIterable payloadBody, HttpSerializer serializer); + /** + * Set the {@link HttpMessageBodyIterable} for this response. + * @param messageBody The serialized message body. + * @param serializer The function that serializes the underlying {@link BlockingIterable}. + * @param The type of the serialized objects. + * @return {@code this} + */ + BlockingStreamingHttpRequest messageBody(HttpMessageBodyIterable messageBody, + HttpStreamingSerializer serializer); + /** * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload transformed to the result of * serialization. @@ -138,10 +198,13 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { * {@link BlockingIterable} and returns the new payload body {@link BlockingIterable} prior to serialization. It is * assumed the existing payload body {@link BlockingIterable} will be transformed/consumed or else no more requests * may be processed. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)} and + * {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated BlockingStreamingHttpRequest transformPayloadBody( Function, BlockingIterable> transformer, HttpSerializer serializer); @@ -152,12 +215,15 @@ BlockingStreamingHttpRequest transformPayloadBody( * {@link BlockingIterable} and returns the new payload body {@link BlockingIterable} prior to serialization. It is * assumed the existing payload body {@link BlockingIterable} will be transformed/consumed or else no more requests * may be processed. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)} and + * {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param deserializer Used to deserialize the existing payload body. * @param serializer Used to serialize the payload body. * @param The type of objects to deserialize. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated default BlockingStreamingHttpRequest transformPayloadBody( Function, BlockingIterable> transformer, HttpDeserializer deserializer, HttpSerializer serializer) { @@ -166,20 +232,24 @@ default BlockingStreamingHttpRequest transformPayloadBody( /** * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload transformed to {@link Buffer}s. + * @deprecated Use {@link #payloadBody()} and {@link #payloadBody(Iterable)}. * @param transformer A {@link Function} which take as a parameter the existing payload body * {@link BlockingIterable} and returns the new payload body {@link BlockingIterable}. It is assumed the existing * payload body {@link BlockingIterable} will be transformed/consumed or else no more requests may be processed. * @return {@code this} */ + @Deprecated BlockingStreamingHttpRequest transformPayloadBody(UnaryOperator> transformer); /** * Returns a {@link BlockingStreamingHttpRequest} with its underlying payload transformed to {@link Buffer}s, * with access to the trailers. + * @deprecated Use {@link #messageBody()} and {@link #messageBody(HttpMessageBodyIterable)}. * @param trailersTransformer {@link TrailersTransformer} to use for this transform. * @param The type of state used during the transformation. * @return {@code this} */ + @Deprecated BlockingStreamingHttpRequest transform(TrailersTransformer trailersTransformer); /** @@ -231,9 +301,13 @@ default BlockingStreamingHttpRequest transformPayloadBody( @Override BlockingStreamingHttpRequest version(HttpProtocolVersion version); + @Deprecated @Override BlockingStreamingHttpRequest encoding(ContentCodec encoding); + @Override + BlockingStreamingHttpRequest contentEncoding(@Nullable BufferEncoder encoder); + @Override BlockingStreamingHttpRequest method(HttpRequestMethod method); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpResponse.java index acdcfa69e3..bf8b9fe5dd 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpResponse.java @@ -46,14 +46,38 @@ default InputStream payloadBodyInputStream() { /** * Gets and deserializes the payload body. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)}. * @param deserializer The function that deserializes the underlying {@link BlockingIterable}. * @param The resulting type of the deserialization operation. * @return The results of the deserialization operation. */ + @Deprecated default BlockingIterable payloadBody(HttpDeserializer deserializer) { return deserializer.deserialize(headers(), payloadBody()); } + /** + * Gets and deserializes the payload body. + * @param deserializer The function that deserializes the underlying {@link BlockingIterable}. + * @param The resulting type of the deserialization operation. + * @return The results of the deserialization operation. + */ + BlockingIterable payloadBody(HttpStreamingDeserializer deserializer); + + /** + * Get the {@link HttpMessageBodyIterable} for this response. + * @return the {@link HttpMessageBodyIterable} for this response. + */ + HttpMessageBodyIterable messageBody(); + + /** + * Get the {@link HttpMessageBodyIterable} for this response and deserialize to type {@link T}. + * @param deserializer The function that deserializes the underlying {@link BlockingIterable}. + * @param The resulting type of the deserialization operation. + * @return the {@link HttpMessageBodyIterable} for this payloadBody. + */ + HttpMessageBodyIterable messageBody(HttpStreamingDeserializer deserializer); + /** * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload set to {@code payloadBody}. *

@@ -96,6 +120,13 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { */ BlockingStreamingHttpResponse payloadBody(InputStream payloadBody); + /** + * Set the {@link HttpMessageBodyIterable} for this response. + * @param messageBody The new message body. + * @return {@code this}. + */ + BlockingStreamingHttpResponse messageBody(HttpMessageBodyIterable messageBody); + /** * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload set to the result of serialization. *

@@ -105,13 +136,30 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { *

* This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the * combination with the existing payload body that is being replaced. + * @deprecated Use {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param payloadBody The new payload body, prior to serialization. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated BlockingStreamingHttpResponse payloadBody(Iterable payloadBody, HttpSerializer serializer); + /** + * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload set to the result of serialization. + *

+ * A best effort will be made to apply back pressure to the existing payload body which is being replaced. If this + * default policy is not sufficient {@link #payloadBody()} can be used to drain with more fine grain control. + *

+ * This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the + * combination with the existing payload body that is being replaced. + * @param payloadBody The new payload body, prior to serialization. + * @param serializer Used to serialize the payload body. + * @param The type of objects to serialize. + * @return {@code this} + */ + BlockingStreamingHttpResponse payloadBody(Iterable payloadBody, HttpStreamingSerializer serializer); + /** * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload set to the result of serialization. *

@@ -121,13 +169,25 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { *

* This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the * combination with the existing payload body that is being replaced. + * @deprecated Use {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param payloadBody The new payload body, prior to serialization. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated BlockingStreamingHttpResponse payloadBody(CloseableIterable payloadBody, HttpSerializer serializer); + /** + * Set the {@link HttpMessageBodyIterable} for this response. + * @param messageBody The serialized message body. + * @param serializer The function that serializes the underlying {@link BlockingIterable}. + * @param The type of the serialized objects. + * @return {@code this} + */ + BlockingStreamingHttpResponse messageBody(HttpMessageBodyIterable messageBody, + HttpStreamingSerializer serializer); + /** * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload transformed to the result of * serialization. @@ -135,10 +195,13 @@ default BlockingIterable payloadBody(HttpDeserializer deserializer) { * {@link BlockingIterable} and returns the new payload body {@link BlockingIterable} prior to serialization. It is * assumed the existing payload body {@link BlockingIterable} will be transformed/consumed or else no more responses * may be processed. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)} and + * {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated BlockingStreamingHttpResponse transformPayloadBody( Function, BlockingIterable> transformer, HttpSerializer serializer); @@ -149,12 +212,15 @@ BlockingStreamingHttpResponse transformPayloadBody( * {@link BlockingIterable} and returns the new payload body {@link BlockingIterable} prior to serialization. It is * assumed the existing payload body {@link BlockingIterable} will be transformed/consumed or else no more requests * may be processed. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)} and + * {@link #payloadBody(Iterable, HttpStreamingSerializer)}. * @param deserializer Used to deserialize the existing payload body. * @param serializer Used to serialize the payload body. * @param The type of objects to deserialize. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated default BlockingStreamingHttpResponse transformPayloadBody( Function, BlockingIterable> transformer, HttpDeserializer deserializer, HttpSerializer serializer) { @@ -163,20 +229,24 @@ default BlockingStreamingHttpResponse transformPayloadBody( /** * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload transformed to {@link Buffer}s. + * @deprecated Use {@link #payloadBody()} and {@link #payloadBody(Iterable)}. * @param transformer A {@link Function} which take as a parameter the existing payload body * {@link BlockingIterable} and returns the new payload body {@link BlockingIterable}. It is assumed the existing * payload body {@link BlockingIterable} will be transformed/consumed or else no more responses may be processed. * @return {@code this} */ + @Deprecated BlockingStreamingHttpResponse transformPayloadBody(UnaryOperator> transformer); /** * Returns a {@link BlockingStreamingHttpResponse} with its underlying payload transformed to {@link Buffer}s, * with access to the trailers. + * @deprecated Use {@link #messageBody()} and {@link #messageBody(HttpMessageBodyIterable)}. * @param trailersTransformer {@link TrailersTransformer} to use for this transform. * @param The type of state used during the transformation. * @return {@code this} */ + @Deprecated BlockingStreamingHttpResponse transform(TrailersTransformer trailersTransformer); /** diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpServerResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpServerResponse.java index aee1b6c616..2ad0405aa0 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpServerResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/BlockingStreamingHttpServerResponse.java @@ -38,7 +38,7 @@ public abstract class BlockingStreamingHttpServerResponse extends DefaultHttpRes * @param status a status for the response * @param version a default version for the response * @param headers an {@link HttpHeaders} object for headers - * @param allocator a {@link BufferAllocator} to use for {@link #sendMetaData(HttpSerializer)} + * @param allocator a {@link BufferAllocator} to use for {@link #sendMetaData(HttpStreamingSerializer)}. */ BlockingStreamingHttpServerResponse(final HttpResponseStatus status, final HttpProtocolVersion version, @@ -66,18 +66,36 @@ public abstract class BlockingStreamingHttpServerResponse extends DefaultHttpRes * to continue writing a payload body. Each element will be serialized using provided {@code serializer}. *

* Note: calling any other method on this class after calling this method is not allowed. - * + * @deprecated Use {@link #sendMetaData(HttpStreamingSerializer)}. * @param serializer used to serialize the payload elements * @param the type of objects to write * @return {@link HttpPayloadWriter} to write a payload body * @throws IllegalStateException if one of the {@code sendMetaData*} methods has been called on this response */ + @Deprecated public final HttpPayloadWriter sendMetaData(final HttpSerializer serializer) { final HttpPayloadWriter payloadWriter = serializer.serialize(headers(), this.payloadWriter, allocator); sendMetaData(); return payloadWriter; } + /** + * Sends the {@link HttpResponseMetaData} to the client and returns an {@link HttpPayloadWriter} of type {@link T} + * to continue writing a payload body. Each element will be serialized using provided {@code serializer}. + *

+ * Note: calling any other method on this class after calling this method is not allowed. + * + * @param serializer used to serialize the payload elements + * @param the type of objects to write + * @return {@link HttpPayloadWriter} to write a payload body + * @throws IllegalStateException if one of the {@code sendMetaData*} methods has been called on this response + */ + public final HttpPayloadWriter sendMetaData(final HttpStreamingSerializer serializer) { + final HttpPayloadWriter payloadWriter = serializer.serialize(headers(), this.payloadWriter, allocator); + sendMetaData(); + return payloadWriter; + } + /** * Sends the {@link HttpResponseMetaData} to the client and returns an {@link OutputStream} to continue writing a * payload body. diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpRequesterFilter.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpRequesterFilter.java index 03e32ec693..1a22a29b7b 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpRequesterFilter.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpRequesterFilter.java @@ -25,19 +25,20 @@ import static io.servicetalk.buffer.api.CharSequences.newAsciiString; import static io.servicetalk.encoding.api.Identity.identity; +import static io.servicetalk.http.api.HeaderUtils.addContentEncoding; import static io.servicetalk.http.api.HeaderUtils.identifyContentEncodingOrNullIfIdentity; import static io.servicetalk.http.api.HeaderUtils.setAcceptEncoding; -import static io.servicetalk.http.api.HeaderUtils.setContentEncoding; /** * A {@link StreamingHttpClientFilter} that adds encoding / decoding functionality for requests and responses * respectively, as these are specified by the spec * Content-Encoding. - * *

* Append this filter before others that are expected to to see compressed content for this request/response, and after * other filters that expect to manipulate the original payload. + * @deprecated Use {@link ContentEncodingHttpRequesterFilter}. */ +@Deprecated public final class ContentCodingHttpRequesterFilter implements StreamingHttpClientFilterFactory, StreamingHttpConnectionFilterFactory, HttpExecutionStrategyInfluencer { @@ -133,7 +134,7 @@ private static void encodePayloadContentIfAvailable(final StreamingHttpRequest r final BufferAllocator allocator) { ContentCodec coding = request.encoding(); if (coding != null && !identity().equals(coding)) { - setContentEncoding(request.headers(), coding.name()); + addContentEncoding(request.headers(), coding.name()); request.transformPayloadBody(pub -> coding.encode(pub, allocator)); } } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpServiceFilter.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpServiceFilter.java index c39bde46bb..8dd1e91128 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpServiceFilter.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentCodingHttpServiceFilter.java @@ -30,9 +30,9 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.encoding.api.Identity.identity; import static io.servicetalk.encoding.api.internal.HeaderUtils.negotiateAcceptedEncoding; +import static io.servicetalk.http.api.HeaderUtils.addContentEncoding; import static io.servicetalk.http.api.HeaderUtils.hasContentEncoding; import static io.servicetalk.http.api.HeaderUtils.identifyContentEncodingOrNullIfIdentity; -import static io.servicetalk.http.api.HeaderUtils.setContentEncoding; import static io.servicetalk.http.api.HttpHeaderNames.ACCEPT_ENCODING; import static io.servicetalk.http.api.HttpRequestMethod.CONNECT; import static io.servicetalk.http.api.HttpRequestMethod.HEAD; @@ -50,7 +50,9 @@ *

* Append this filter before others that are expected to to see compressed content for this request/response, and after * other filters that expect to see/manipulate the original payload. + * @deprecated Use {@link ContentEncodingHttpServiceFilter}. */ +@Deprecated public final class ContentCodingHttpServiceFilter implements StreamingHttpServiceFilterFactory, HttpExecutionStrategyInfluencer { @@ -144,7 +146,7 @@ private static void encodePayloadContentIfAvailable(final HttpHeaders requestHea ContentCodec coding = codingForResponse(requestHeaders, response, supportedEncodings); if (coding != null) { - setContentEncoding(response.headers(), coding.name()); + addContentEncoding(response.headers(), coding.name()); response.transformPayloadBody(bufferPublisher -> coding.encode(bufferPublisher, allocator)); } } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentEncodingHttpRequesterFilter.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentEncodingHttpRequesterFilter.java new file mode 100644 index 0000000000..aa3bb084f9 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentEncodingHttpRequesterFilter.java @@ -0,0 +1,126 @@ +/* + * Copyright © 2020-2021 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.http.api; + +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferEncoder; + +import java.util.Iterator; + +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.http.api.ContentEncodingHttpServiceFilter.matchAndRemoveEncoding; +import static io.servicetalk.http.api.HeaderUtils.addContentEncoding; +import static io.servicetalk.http.api.HttpHeaderNames.ACCEPT_ENCODING; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_ENCODING; +import static java.util.Objects.requireNonNull; + +/** + * A {@link StreamingHttpClientFilter} that adds encoding / decoding functionality for requests and responses + * respectively, as these are specified by the spec + * Content-Encoding. + * + *

+ * Append this filter before others that are expected to to see compressed content for this request/response, and after + * other filters that expect to manipulate the original payload. + */ +public final class ContentEncodingHttpRequesterFilter implements + StreamingHttpClientFilterFactory, StreamingHttpConnectionFilterFactory, HttpExecutionStrategyInfluencer { + private final BufferDecoderGroup decompressors; + + /** + * Create a new instance and specify the supported decompression (advertised in + * {@link HttpHeaderNames#ACCEPT_ENCODING}). The compression is specified via + * {@link HttpRequestMetaData#contentEncoding()}. The order of entries may impact the selection preference. + * + * @param decompressors the decompression supported to decode responses accordingly and also used to advertise + * {@link HttpHeaderNames#ACCEPT_ENCODING} to the server. + */ + public ContentEncodingHttpRequesterFilter(final BufferDecoderGroup decompressors) { + this.decompressors = requireNonNull(decompressors); + } + + @Override + public StreamingHttpClientFilter create(final FilterableStreamingHttpClient client) { + return new StreamingHttpClientFilter(client) { + @Override + protected Single request(final StreamingHttpRequester delegate, + final HttpExecutionStrategy strategy, + final StreamingHttpRequest request) { + return applyEncodingAndDecoding(delegate, strategy, request); + } + }; + } + + @Override + public StreamingHttpConnectionFilter create(final FilterableStreamingHttpConnection connection) { + return new StreamingHttpConnectionFilter(connection) { + @Override + public Single request(final HttpExecutionStrategy strategy, + final StreamingHttpRequest request) { + return applyEncodingAndDecoding(delegate(), strategy, request); + } + }; + } + + @Override + public HttpExecutionStrategy influenceStrategy(final HttpExecutionStrategy strategy) { + // No influence since we do not block. + return strategy; + } + + private Single applyEncodingAndDecoding(final StreamingHttpRequester delegate, + final HttpExecutionStrategy strategy, + final StreamingHttpRequest request) { + return Single.defer(() -> { + boolean decompressResponse = false; + CharSequence encodings = decompressors.advertisedMessageEncoding(); + if (encodings != null && !request.headers().contains(ACCEPT_ENCODING)) { + request.headers().set(ACCEPT_ENCODING, encodings); + decompressResponse = true; + } + BufferEncoder encoder = request.contentEncoding(); + final StreamingHttpRequest encodedRequest; + if (encoder != null && !identityEncoder().equals(encoder)) { + addContentEncoding(request.headers(), encoder.encodingName()); + encodedRequest = request.transformPayloadBody(pub -> encoder.streamingEncoder().serialize(pub, + delegate.executionContext().bufferAllocator())); + } else { + encodedRequest = request; + } + + Single respSingle = delegate.request(strategy, encodedRequest); + return (decompressResponse ? respSingle.map(response -> { + Iterator contentEncodingItr = + response.headers().valuesIterator(CONTENT_ENCODING); + final boolean hasContentEncoding = contentEncodingItr.hasNext(); + if (!hasContentEncoding) { + return response; + } + BufferDecoder decoder = matchAndRemoveEncoding(decompressors.decoders(), + BufferDecoder::encodingName, contentEncodingItr, response.headers()); + if (decoder == null) { + throw new UnsupportedContentEncodingException(response.headers().get(CONTENT_ENCODING, + "").toString()); + } + + return response.transformPayloadBody(pub -> decoder.streamingDecoder().deserialize(pub, + delegate.executionContext().bufferAllocator())); + }) : respSingle).subscribeShareContext(); + }); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentEncodingHttpServiceFilter.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentEncodingHttpServiceFilter.java new file mode 100644 index 0000000000..a61054478d --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/ContentEncodingHttpServiceFilter.java @@ -0,0 +1,233 @@ +/* + * Copyright © 2020-2021 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.http.api; + +import io.servicetalk.buffer.api.CharSequences; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferDecoder; +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.encoding.api.EmptyBufferDecoderGroup; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import javax.annotation.Nullable; + +import static io.servicetalk.buffer.api.CharSequences.regionMatches; +import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.encoding.api.internal.HeaderUtils.negotiateAcceptedEncodingRaw; +import static io.servicetalk.http.api.HeaderUtils.addContentEncoding; +import static io.servicetalk.http.api.HttpHeaderNames.ACCEPT_ENCODING; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_ENCODING; +import static io.servicetalk.http.api.HttpRequestMethod.CONNECT; +import static io.servicetalk.http.api.HttpRequestMethod.HEAD; +import static io.servicetalk.http.api.HttpResponseStatus.NOT_MODIFIED; +import static io.servicetalk.http.api.HttpResponseStatus.NO_CONTENT; +import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.INFORMATIONAL_1XX; +import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.SUCCESSFUL_2XX; +import static java.util.Objects.requireNonNull; + +/** + * A {@link StreamingHttpService} that adds encoding / decoding functionality for responses and requests respectively, + * as these are specified by the spec + * Content-Encoding. + * + *

+ * Append this filter before others that are expected to to see compressed content for this request/response, and after + * other filters that expect to see/manipulate the original payload. + */ +public final class ContentEncodingHttpServiceFilter + implements StreamingHttpServiceFilterFactory, HttpExecutionStrategyInfluencer { + private final BufferDecoderGroup decompressors; + private final List compressors; + + /** + * Create a new instance and specify the supported compression (matched against + * {@link HttpHeaderNames#ACCEPT_ENCODING}). The order of entries may impact the selection preference. + * + * @param compressors used to compress server responses if client accepts them. + */ + public ContentEncodingHttpServiceFilter(final List compressors) { + this(compressors, EmptyBufferDecoderGroup.INSTANCE); + } + + /** + * Create a new instance and specify the supported decompression (matched against + * {@link HttpHeaderNames#CONTENT_ENCODING}) and compression (matched against + * {@link HttpHeaderNames#ACCEPT_ENCODING}). The order of entries may impact the selection preference. + * + * @param compressors used to compress server responses if client accepts them. + * @param decompressors used to decompress client requests if compressed. + */ + public ContentEncodingHttpServiceFilter(final List compressors, + final BufferDecoderGroup decompressors) { + this.decompressors = requireNonNull(decompressors); + this.compressors = requireNonNull(compressors); + } + + @Override + public StreamingHttpServiceFilter create(final StreamingHttpService service) { + return new StreamingHttpServiceFilter(service) { + @Override + public Single handle(final HttpServiceContext ctx, + final StreamingHttpRequest request, + final StreamingHttpResponseFactory responseFactory) { + return Single.defer(() -> { + final StreamingHttpRequest requestDecompressed; + Iterator contentEncodingItr = + request.headers().valuesIterator(CONTENT_ENCODING); + final boolean hasContentEncoding = contentEncodingItr.hasNext(); + if (hasContentEncoding) { + BufferDecoder decoder = matchAndRemoveEncoding(decompressors.decoders(), + BufferDecoder::encodingName, contentEncodingItr, request.headers()); + if (decoder == null) { + return succeeded(responseFactory.unsupportedMediaType()); + } + + requestDecompressed = request.transformPayloadBody(pub -> + decoder.streamingDecoder().deserialize(pub, ctx.executionContext().bufferAllocator())); + } else { + requestDecompressed = request; + } + + return super.handle(ctx, requestDecompressed, responseFactory).map(response -> { + final CharSequence reqAcceptEncoding; + if (isPassThrough(request.method(), response) || + (reqAcceptEncoding = request.headers().get(ACCEPT_ENCODING)) == null) { + return response; + } + + BufferEncoder encoder = negotiateAcceptedEncodingRaw(reqAcceptEncoding, compressors, + BufferEncoder::encodingName); + if (encoder == null || identityEncoder().equals(encoder)) { + return response; + } + + addContentEncoding(response.headers(), encoder.encodingName()); + return response.transformPayloadBody(bufPub -> + encoder.streamingEncoder().serialize(bufPub, ctx.executionContext().bufferAllocator())); + }).subscribeShareContext(); + }); + } + }; + } + + @Override + public HttpExecutionStrategy influenceStrategy(final HttpExecutionStrategy strategy) { + // No influence - no blocking + return strategy; + } + + private static boolean isPassThrough(final HttpRequestMethod method, final StreamingHttpResponse response) { + // see. https://tools.ietf.org/html/rfc7230#section-3.3.3 + // The length of a message body is determined by one of the following + // (in order of precedence): + // + // 1. Any response to a HEAD request and any response with a 1xx + // (Informational), 204 (No Content), or 304 (Not Modified) status + // code is always terminated by the first empty line after the + // header fields, regardless of the header fields present in the + // message, and thus cannot contain a message body. + // + // 2. Any 2xx (Successful) response to a CONNECT request implies that + // the connection will become a tunnel immediately after the empty + // line that concludes the header fields. A client MUST ignore any + // Content-Length or Transfer-Encoding header fields received in + // such a message. + // ... + final int code = response.status().code(); + return INFORMATIONAL_1XX.contains(code) || code == NO_CONTENT.code() || code == NOT_MODIFIED.code() || + (method == HEAD || (method == CONNECT && SUCCESSFUL_2XX.contains(code))); + } + + @Nullable + static T matchAndRemoveEncoding(final List supportedEncoders, + final Function messageEncodingFunc, + final Iterator contentEncodingItr, + final HttpHeaders headers) { + // The order of headers is meaningful for decompression [1], the first header value must be supported or we + // cannot decode. + // [1] If one or more encodings have been applied to a representation, the + // sender that applied the encodings MUST generate a Content-Encoding + // header field that lists the content codings in the order in which + // they were applied + // https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.2.2 + if (supportedEncoders.isEmpty() || !contentEncodingItr.hasNext()) { + return null; + } + final CharSequence encoding = contentEncodingItr.next(); + int jNonTrimmed; + int i = 0; + int j = CharSequences.indexOf(encoding, ',', 0); + if (j < 0) { + j = encoding.length(); + } + + if (j == 0) { + return null; + } + + jNonTrimmed = j; + // Trim spaces from end. + while (encoding.charAt(j - 1) == ' ') { + if (--j == 0) { + return null; + } + } + + // Trim spaces from beginning. + while (encoding.charAt(i) == ' ') { + if (++i == j) { + return null; + } + } + + // If the accepted encoding is supported, use it. + int x = 0; + do { + T supportedEncoding = supportedEncoders.get(x); + CharSequence serverSupported = messageEncodingFunc.apply(supportedEncoding); + // Use serverSupported.length() as we ignore qvalue prioritization for now. + // All content-coding values are case-insensitive [1]. + // [1] https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.2.1. + if (regionMatches(encoding, true, i, serverSupported, 0, serverSupported.length())) { + // We will be stripping the encoding, so remove the header. + contentEncodingItr.remove(); + if (jNonTrimmed + 1 < encoding.length()) { + // Order of content-encodings must be preserved, so if the value is CSV add back the remainder of + // the unused value. + resetContentEncoding(headers, encoding.subSequence(jNonTrimmed + 1, encoding.length())); + } + + return supportedEncoding; + } + } while (++x < supportedEncoders.size()); + + return null; + } + + private static void resetContentEncoding(HttpHeaders headers, CharSequence updatedValue) { + List valuesArray = new ArrayList<>(4); + valuesArray.add(updatedValue); + for (CharSequence value : headers.values(CONTENT_ENCODING)) { + valuesArray.add(value); + } + headers.set(CONTENT_ENCODING, valuesArray); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpRequest.java index 77b1b001fa..ab02898e99 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpRequest.java @@ -19,16 +19,21 @@ import io.servicetalk.concurrent.BlockingIterable; import io.servicetalk.concurrent.CloseableIterable; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.Objects; import java.util.function.Function; import java.util.function.UnaryOperator; import javax.annotation.Nullable; +import static io.servicetalk.concurrent.api.Publisher.defer; +import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Publisher.fromInputStream; import static io.servicetalk.concurrent.api.Publisher.fromIterable; +import static io.servicetalk.http.api.BlockingStreamingHttpMessageBodyUtils.newMessageBody; final class DefaultBlockingStreamingHttpRequest extends AbstractDelegatingHttpRequest implements BlockingStreamingHttpRequest { @@ -43,12 +48,19 @@ public BlockingStreamingHttpRequest version(final HttpProtocolVersion version) { return this; } + @Deprecated @Override public BlockingStreamingHttpRequest encoding(final ContentCodec encoding) { original.encoding(encoding); return this; } + @Override + public BlockingStreamingHttpRequest contentEncoding(@Nullable final BufferEncoder encoder) { + original.contentEncoding(encoder); + return this; + } + @Override public BlockingStreamingHttpRequest method(final HttpRequestMethod method) { original.method(method); @@ -138,6 +150,22 @@ public BlockingIterable payloadBody() { return original.payloadBody().toIterable(); } + @Override + public BlockingIterable payloadBody(final HttpStreamingDeserializer deserializer) { + return deserializer.deserialize(headers(), payloadBody(), original.payloadHolder().allocator()); + } + + @Override + public HttpMessageBodyIterable messageBody() { + return newMessageBody(original.messageBody().toIterable()); + } + + @Override + public HttpMessageBodyIterable messageBody(final HttpStreamingDeserializer deserializer) { + return newMessageBody(original.messageBody().toIterable(), headers(), deserializer, + original.payloadHolder().allocator()); + } + @Override public BlockingStreamingHttpRequest payloadBody(final Iterable payloadBody) { original.payloadBody(fromIterable(payloadBody)); @@ -157,20 +185,53 @@ public BlockingStreamingHttpRequest payloadBody(final InputStream payloadBody) { return this; } + @Override + public BlockingStreamingHttpRequest messageBody(final HttpMessageBodyIterable messageBody) { + original.payloadHolder().messageBody(defer(() -> { + HttpMessageBodyIterator body = messageBody.iterator(); + return fromIterable(() -> body) + .map(o -> (Object) o) + .concat(defer(() -> from(body.trailers()).filter(Objects::nonNull))); + })); + return this; + } + + @Override + public BlockingStreamingHttpRequest messageBody(final HttpMessageBodyIterable messageBody, + final HttpStreamingSerializer serializer) { + original.payloadHolder().messageBody(defer(() -> { + HttpMessageBodyIterator body = messageBody.iterator(); + return from(serializer.serialize(headers(), () -> body, original.payloadHolder().allocator())) + .map(o -> (Object) o) + .concat(defer(() -> from(body.trailers()).filter(Objects::nonNull))); + })); + return this; + } + + @Deprecated + @Override + public BlockingStreamingHttpRequest payloadBody(final Iterable payloadBody, + final HttpSerializer serializer) { + original.payloadBody(fromIterable(payloadBody), serializer); + return this; + } + @Override public BlockingStreamingHttpRequest payloadBody(final Iterable payloadBody, - final HttpSerializer serializer) { + final HttpStreamingSerializer serializer) { original.payloadBody(fromIterable(payloadBody), serializer); return this; } + @Deprecated @Override public BlockingStreamingHttpRequest payloadBody(final CloseableIterable payloadBody, - final HttpSerializer serializer) { + final HttpSerializer serializer) { original.payloadBody(fromIterable(payloadBody), serializer); return this; } + @Deprecated @Override public BlockingStreamingHttpRequest transformPayloadBody( final Function, BlockingIterable> transformer, @@ -187,6 +248,7 @@ public BlockingStreamingHttpRequest transformPayloadBody( return this; } + @Deprecated @Override public BlockingStreamingHttpRequest transform(final TrailersTransformer trailersTransformer) { original.transform(trailersTransformer); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpResponse.java index 6828f29fd1..c55a76aa76 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultBlockingStreamingHttpResponse.java @@ -22,11 +22,15 @@ import io.servicetalk.encoding.api.ContentCodec; import java.io.InputStream; +import java.util.Objects; import java.util.function.Function; import java.util.function.UnaryOperator; +import static io.servicetalk.concurrent.api.Publisher.defer; +import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Publisher.fromInputStream; import static io.servicetalk.concurrent.api.Publisher.fromIterable; +import static io.servicetalk.http.api.BlockingStreamingHttpMessageBodyUtils.newMessageBody; final class DefaultBlockingStreamingHttpResponse extends AbstractDelegatingHttpResponse implements BlockingStreamingHttpResponse { @@ -40,6 +44,22 @@ public BlockingIterable payloadBody() { return original.payloadBody().toIterable(); } + @Override + public BlockingIterable payloadBody(final HttpStreamingDeserializer deserializer) { + return deserializer.deserialize(headers(), payloadBody(), original.payloadHolder().allocator()); + } + + @Override + public HttpMessageBodyIterable messageBody() { + return newMessageBody(original.messageBody().toIterable()); + } + + @Override + public HttpMessageBodyIterable messageBody(final HttpStreamingDeserializer deserializer) { + return newMessageBody(original.messageBody().toIterable(), headers(), deserializer, + original.payloadHolder().allocator()); + } + @Override public BlockingStreamingHttpResponse payloadBody(final Iterable payloadBody) { original.payloadBody(fromIterable(payloadBody)); @@ -59,6 +79,30 @@ public BlockingStreamingHttpResponse payloadBody(final InputStream payloadBody) return this; } + @Override + public BlockingStreamingHttpResponse messageBody(final HttpMessageBodyIterable messageBody) { + original.payloadHolder().messageBody(defer(() -> { + HttpMessageBodyIterator body = messageBody.iterator(); + return fromIterable(() -> body) + .map(o -> (Object) o) + .concat(defer(() -> from(body.trailers()).filter(Objects::nonNull))); + })); + return this; + } + + @Override + public BlockingStreamingHttpResponse messageBody(final HttpMessageBodyIterable messageBody, + final HttpStreamingSerializer serializer) { + original.payloadHolder().messageBody(defer(() -> { + HttpMessageBodyIterator body = messageBody.iterator(); + return from(serializer.serialize(headers(), () -> body, original.payloadHolder().allocator())) + .map(o -> (Object) o) + .concat(defer(() -> from(body.trailers()).filter(Objects::nonNull))); + })); + return this; + } + + @Deprecated @Override public BlockingStreamingHttpResponse payloadBody(final Iterable payloadBody, final HttpSerializer serializer) { @@ -66,6 +110,14 @@ public BlockingStreamingHttpResponse payloadBody(final Iterable payloadBo return this; } + @Override + public BlockingStreamingHttpResponse payloadBody(final Iterable payloadBody, + final HttpStreamingSerializer serializer) { + original.payloadBody(fromIterable(payloadBody), serializer); + return this; + } + + @Deprecated @Override public BlockingStreamingHttpResponse payloadBody(final CloseableIterable payloadBody, final HttpSerializer serializer) { @@ -73,6 +125,7 @@ public BlockingStreamingHttpResponse payloadBody(final CloseableIterable return this; } + @Deprecated @Override public BlockingStreamingHttpResponse transformPayloadBody(final Function, BlockingIterable> transformer, final HttpSerializer serializer) { @@ -88,6 +141,7 @@ public BlockingStreamingHttpResponse transformPayloadBody( return this; } + @Deprecated @Override public BlockingStreamingHttpResponse transform(final TrailersTransformer trailersTransformer) { original.transform(trailersTransformer); @@ -110,6 +164,7 @@ public BlockingStreamingHttpResponse version(final HttpProtocolVersion version) return this; } + @Deprecated @Override public BlockingStreamingHttpResponse encoding(final ContentCodec encoding) { original.encoding(encoding); @@ -121,28 +176,4 @@ public BlockingStreamingHttpResponse status(final HttpResponseStatus status) { original.status(status); return this; } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - - final DefaultBlockingStreamingHttpResponse that = (DefaultBlockingStreamingHttpResponse) o; - - return original.equals(that.original); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + original.hashCode(); - return result; - } } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpDeserializer.java index 4237add0c5..fbce47c27a 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpDeserializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpDeserializer.java @@ -26,10 +26,11 @@ /** * A {@link HttpDeserializer} that can deserialize to a {@link Class} of type {@link T}. - * + * @deprecated Will be removed with {@link HttpDeserializer}. * @param Type to deserialize. * @see DefaultTypeHttpSerializer */ +@Deprecated final class DefaultClassHttpDeserializer implements HttpDeserializer { private final Serializer serializer; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpSerializer.java index 6c8b65dd1f..b06b774bca 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpSerializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultClassHttpSerializer.java @@ -26,11 +26,12 @@ /** * An {@link HttpSerializer} that serializes a {@link Class} of type {@link T}. - * + * @deprecated Will be removed with {@link HttpSerializer}. * @param Type to serialize * @see DefaultTypeHttpSerializer * @see DefaultSizeAwareClassHttpSerializer */ +@Deprecated final class DefaultClassHttpSerializer implements HttpSerializer { private final Consumer addContentType; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequest.java index 18bd3d75ba..884044f3ff 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequest.java @@ -17,6 +17,7 @@ import io.servicetalk.buffer.api.Buffer; import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.nio.charset.Charset; @@ -46,12 +47,19 @@ public HttpRequest version(final HttpProtocolVersion version) { return this; } + @Deprecated @Override public HttpRequest encoding(final ContentCodec encoding) { original.encoding(encoding); return this; } + @Override + public HttpRequest contentEncoding(@Nullable final BufferEncoder encoder) { + original.contentEncoding(encoder); + return this; + } + @Override public HttpRequest method(final HttpRequestMethod method) { original.method(method); @@ -194,6 +202,11 @@ public Buffer payloadBody() { return payloadBody; } + @Override + public T payloadBody(final HttpDeserializer2 deserializer) { + return deserializer.deserialize(headers(), original.payloadHolder().allocator(), payloadBody); + } + @Override public HttpRequest payloadBody(final Buffer payloadBody) { this.payloadBody = requireNonNull(payloadBody); @@ -208,6 +221,13 @@ public HttpRequest payloadBody(final T pojo, final HttpSerializer seriali return this; } + @Override + public HttpRequest payloadBody(final T pojo, final HttpSerializer2 serializer) { + this.payloadBody = serializer.serialize(headers(), pojo, original.payloadHolder().allocator()); + original.payloadBody(from(payloadBody)); + return this; + } + @Override public HttpHeaders trailers() { if (trailers == null) { @@ -251,7 +271,8 @@ public StreamingHttpRequest toStreamingRequest() { final DefaultPayloadInfo payloadInfo = new DefaultPayloadInfo(this).setEmpty(emptyPayloadBody) .setMayHaveTrailersAndGenericTypeBuffer(trailers != null); return new DefaultStreamingHttpRequest(method(), requestTarget(), version(), headers(), encoding(), - original.payloadHolder().allocator(), payload, payloadInfo, original.payloadHolder().headersFactory()); + contentEncoding(), original.payloadHolder().allocator(), payload, payloadInfo, + original.payloadHolder().headersFactory()); } @Override diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequestMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequestMetaData.java index a75964cd14..a7f29f22ae 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequestMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpRequestMetaData.java @@ -15,6 +15,7 @@ */ package io.servicetalk.http.api; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import io.servicetalk.transport.api.HostAndPort; @@ -56,6 +57,8 @@ class DefaultHttpRequestMetaData extends AbstractHttpMetaData implements HttpReq private String pathDecoded; @Nullable private String queryDecoded; + @Nullable + private BufferEncoder encoder; DefaultHttpRequestMetaData(final HttpRequestMethod method, final String requestTarget, final HttpProtocolVersion version, final HttpHeaders headers) { @@ -70,12 +73,25 @@ public HttpRequestMetaData version(final HttpProtocolVersion version) { return this; } + @Deprecated @Override public HttpMetaData encoding(final ContentCodec encoding) { super.encoding(encoding); return this; } + @Nullable + @Override + public BufferEncoder contentEncoding() { + return encoder; + } + + @Override + public HttpRequestMetaData contentEncoding(@Nullable final BufferEncoder encoder) { + this.encoder = encoder; + return this; + } + @Override public final HttpRequestMethod method() { return method; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponse.java index 9cfa6237c2..1443dad5d0 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponse.java @@ -45,6 +45,7 @@ public HttpResponse version(final HttpProtocolVersion version) { return this; } + @Deprecated @Override public HttpResponse encoding(final ContentCodec encoding) { original.encoding(encoding); @@ -67,6 +68,11 @@ public Buffer payloadBody() { return payloadBody; } + @Override + public T payloadBody(final HttpDeserializer2 deserializer) { + return deserializer.deserialize(headers(), original.payloadHolder().allocator(), payloadBody); + } + @Override public HttpResponse payloadBody(final Buffer payloadBody) { this.payloadBody = requireNonNull(payloadBody); @@ -81,6 +87,13 @@ public HttpResponse payloadBody(final T pojo, final HttpSerializer serial return this; } + @Override + public HttpResponse payloadBody(final T pojo, final HttpSerializer2 serializer) { + this.payloadBody = serializer.serialize(headers(), pojo, original.payloadHolder().allocator()); + original.payloadBody(from(payloadBody)); + return this; + } + @Override public StreamingHttpResponse toStreamingResponse() { final boolean emptyPayloadBody = isAlwaysEmpty(payloadBody); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponseMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponseMetaData.java index 38efa01e85..55b57263ae 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponseMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpResponseMetaData.java @@ -27,11 +27,6 @@ class DefaultHttpResponseMetaData extends AbstractHttpMetaData implements HttpRe this.status = requireNonNull(status); } - DefaultHttpResponseMetaData(final DefaultHttpResponseMetaData responseMetaData) { - super(responseMetaData); - this.status = responseMetaData.status; - } - @Override public HttpResponseMetaData version(final HttpProtocolVersion version) { super.version(version); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializationProvider.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializationProvider.java index 5cdf40f322..ca27679c70 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializationProvider.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializationProvider.java @@ -22,6 +22,10 @@ import java.util.function.IntUnaryOperator; import java.util.function.Predicate; +/** + * @deprecated Will be removed with {@link HttpSerializationProvider}. + */ +@Deprecated final class DefaultHttpSerializationProvider implements HttpSerializationProvider { private final Serializer serializer; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializerDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializerDeserializer.java new file mode 100644 index 0000000000..286841ba22 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpSerializerDeserializer.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static io.servicetalk.http.api.HeaderUtils.deserializeCheckContentType; +import static java.util.Objects.requireNonNull; + +final class DefaultHttpSerializerDeserializer implements HttpSerializerDeserializer { + private final SerializerDeserializer serializer; + private final Consumer headersSerializeConsumer; + private final Predicate headersDeserializePredicate; + + DefaultHttpSerializerDeserializer(final SerializerDeserializer serializer, + final Consumer headersSerializeConsumer, + final Predicate headersDeserializePredicate) { + this.serializer = requireNonNull(serializer); + this.headersSerializeConsumer = requireNonNull(headersSerializeConsumer); + this.headersDeserializePredicate = requireNonNull(headersDeserializePredicate); + } + + @Override + public T deserialize(final HttpHeaders headers, final BufferAllocator allocator, final Buffer payload) { + deserializeCheckContentType(headers, headersDeserializePredicate); + return serializer.deserialize(payload, allocator); + } + + @Override + public Buffer serialize(final HttpHeaders headers, final T value, final BufferAllocator allocator) { + headersSerializeConsumer.accept(headers); + return serializer.serialize(value, allocator); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpStreamingSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpStreamingSerializer.java new file mode 100644 index 0000000000..1290ab1da6 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpStreamingSerializer.java @@ -0,0 +1,100 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.oio.api.PayloadWriter; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.StreamingSerializer; + +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.ToIntFunction; + +import static java.util.Objects.requireNonNull; + +final class DefaultHttpStreamingSerializer implements HttpStreamingSerializer { + private final StreamingSerializer serializer; + private final Consumer headersSerializeConsumer; + + DefaultHttpStreamingSerializer(final Serializer serializer, + final ToIntFunction bytesEstimator, + final Consumer headersSerializeConsumer) { + this(new NonFramedStreamingSerializer<>(serializer, bytesEstimator), headersSerializeConsumer); + } + + DefaultHttpStreamingSerializer(final StreamingSerializer serializer, + final Consumer headersSerializeConsumer) { + this.serializer = requireNonNull(serializer); + this.headersSerializeConsumer = requireNonNull(headersSerializeConsumer); + } + + @Override + public BlockingIterable serialize(final HttpHeaders headers, final BlockingIterable value, + final BufferAllocator allocator) { + // Do the headers modification eagerly, otherwise if this is done lazily the headers would have already been + // written and the modification will not be applied in time. + headersSerializeConsumer.accept(headers); + return serializer.serialize(value, allocator); + } + + @Override + public Publisher serialize(final HttpHeaders headers, final Publisher value, + final BufferAllocator allocator) { + // Do the headers modification eagerly, otherwise if this is done lazily the headers would have already been + // written and the modification will not be applied in time. + headersSerializeConsumer.accept(headers); + return serializer.serialize(value, allocator); + } + + @Override + public HttpPayloadWriter serialize(final HttpHeaders headers, final HttpPayloadWriter payloadWriter, + final BufferAllocator allocator) { + // Do the headers modification eagerly, otherwise if this is done lazily the headers would have already been + // written and the modification will not be applied in time. + headersSerializeConsumer.accept(headers); + PayloadWriter result = serializer.serialize(payloadWriter, allocator); + return new HttpPayloadWriter() { + @Override + public HttpHeaders trailers() { + return payloadWriter.trailers(); + } + + @Override + public void write(final T t) throws IOException { + result.write(t); + } + + @Override + public void close(final Throwable cause) throws IOException { + result.close(cause); + } + + @Override + public void close() throws IOException { + result.close(); + } + + @Override + public void flush() throws IOException { + result.flush(); + } + }; + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpStreamingSerializerDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpStreamingSerializerDeserializer.java new file mode 100644 index 0000000000..789b49be83 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultHttpStreamingSerializerDeserializer.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.oio.api.PayloadWriter; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static io.servicetalk.http.api.HeaderUtils.deserializeCheckContentType; +import static java.util.Objects.requireNonNull; + +final class DefaultHttpStreamingSerializerDeserializer implements HttpStreamingSerializerDeserializer { + private final StreamingSerializerDeserializer serializer; + private final Consumer headersSerializeConsumer; + private final Predicate headersDeserializePredicate; + + DefaultHttpStreamingSerializerDeserializer(final StreamingSerializerDeserializer serializer, + final Consumer headersSerializeConsumer, + final Predicate headersDeserializePredicate) { + this.serializer = requireNonNull(serializer); + this.headersSerializeConsumer = requireNonNull(headersSerializeConsumer); + this.headersDeserializePredicate = requireNonNull(headersDeserializePredicate); + } + + @Override + public BlockingIterable serialize(final HttpHeaders headers, final BlockingIterable value, + final BufferAllocator allocator) { + // Do the headers modification eagerly, otherwise if this is done lazily the headers would have already been + // written and the modification will not be applied in time. + headersSerializeConsumer.accept(headers); + return serializer.serialize(value, allocator); + } + + @Override + public Publisher serialize(final HttpHeaders headers, final Publisher value, + final BufferAllocator allocator) { + // Do the headers modification eagerly, otherwise if this is done lazily the headers would have already been + // written and the modification will not be applied in time. + headersSerializeConsumer.accept(headers); + return serializer.serialize(value, allocator); + } + + @Override + public HttpPayloadWriter serialize(final HttpHeaders headers, final HttpPayloadWriter payloadWriter, + final BufferAllocator allocator) { + // Do the headers modification eagerly, otherwise if this is done lazily the headers would have already been + // written and the modification will not be applied in time. + headersSerializeConsumer.accept(headers); + PayloadWriter result = serializer.serialize(payloadWriter, allocator); + return new HttpPayloadWriter() { + @Override + public HttpHeaders trailers() { + return payloadWriter.trailers(); + } + + @Override + public void write(final T t) throws IOException { + result.write(t); + } + + @Override + public void close(final Throwable cause) throws IOException { + result.close(cause); + } + + @Override + public void close() throws IOException { + result.close(); + } + + @Override + public void flush() throws IOException { + result.flush(); + } + }; + } + + @Override + public BlockingIterable deserialize(final HttpHeaders headers, final BlockingIterable payload, + final BufferAllocator allocator) { + deserializeCheckContentType(headers, headersDeserializePredicate); + return serializer.deserialize(payload, allocator); + } + + @Override + public Publisher deserialize(final HttpHeaders headers, final Publisher payload, + final BufferAllocator allocator) { + deserializeCheckContentType(headers, headersDeserializePredicate); + return serializer.deserialize(payload, allocator); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareClassHttpSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareClassHttpSerializer.java index 3647ab2ef3..05d8a16da1 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareClassHttpSerializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareClassHttpSerializer.java @@ -28,11 +28,12 @@ /** * An {@link HttpSerializer} that serializes a {@link Class} of type {@link T}. This {@link HttpSerializer} can control * sizes of intermediary {@link Buffer}s for serializing a stream. - * + * @deprecated Will be removed with {@link HttpSerializer}. * @param Type to serialize * @see DefaultTypeHttpSerializer * @see DefaultClassHttpSerializer */ +@Deprecated final class DefaultSizeAwareClassHttpSerializer implements HttpSerializer { private final Consumer addContentType; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareTypeHttpSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareTypeHttpSerializer.java index a7864cf38a..77c1968d66 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareTypeHttpSerializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultSizeAwareTypeHttpSerializer.java @@ -29,11 +29,12 @@ /** * An {@link HttpSerializer} that serializes a {@link TypeHolder} of type {@link T}. This {@link HttpSerializer} can * control sizes of intermediary {@link Buffer}s for serializing a stream. - * + * @deprecated Will be removed with {@link HttpSerializer}. * @param Type to serialize * @see DefaultClassHttpSerializer * @see DefaultTypeHttpSerializer */ +@Deprecated final class DefaultSizeAwareTypeHttpSerializer implements HttpSerializer { private final Consumer addContentType; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpRequest.java index e5cc62a91e..9294421e66 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpRequest.java @@ -19,6 +19,7 @@ import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.nio.charset.Charset; @@ -33,14 +34,15 @@ final class DefaultStreamingHttpRequest extends DefaultHttpRequestMetaData DefaultStreamingHttpRequest(final HttpRequestMethod method, final String requestTarget, final HttpProtocolVersion version, final HttpHeaders headers, - @Nullable final ContentCodec encoding, final BufferAllocator allocator, - @Nullable final Publisher payloadBody, final DefaultPayloadInfo payloadInfo, - final HttpHeadersFactory headersFactory) { + @Nullable final ContentCodec encoding, @Nullable final BufferEncoder encoder, + final BufferAllocator allocator, @Nullable final Publisher payloadBody, + final DefaultPayloadInfo payloadInfo, final HttpHeadersFactory headersFactory) { super(method, requestTarget, version, headers); if (encoding != null) { encoding(encoding); } payloadHolder = new StreamingHttpPayloadHolder(headers, allocator, payloadBody, payloadInfo, headersFactory); + this.contentEncoding(encoder); } @Override @@ -49,12 +51,19 @@ public StreamingHttpRequest version(final HttpProtocolVersion version) { return this; } + @Deprecated @Override public StreamingHttpRequest encoding(final ContentCodec encoding) { super.encoding(encoding); return this; } + @Override + public StreamingHttpRequest contentEncoding(@Nullable final BufferEncoder encoder) { + super.contentEncoding(encoder); + return this; + } + @Override public StreamingHttpRequest method(final HttpRequestMethod method) { super.method(method); @@ -144,6 +153,11 @@ public Publisher payloadBody() { return payloadHolder.payloadBody(); } + @Override + public Publisher payloadBody(final HttpStreamingDeserializer deserializer) { + return deserializer.deserialize(headers(), payloadBody(), payloadHolder.allocator()); + } + @Override public Publisher messageBody() { return payloadHolder.messageBody(); @@ -155,20 +169,46 @@ public StreamingHttpRequest payloadBody(final Publisher payloadBody) { return this; } + @Deprecated + @Override + public StreamingHttpRequest payloadBody(final Publisher payloadBody, final HttpSerializer serializer) { + payloadHolder.transformPayloadBody(bufPub -> + serializer.serialize(headers(), payloadBody, payloadHolder.allocator())); + return this; + } + @Override public StreamingHttpRequest payloadBody(final Publisher payloadBody, - final HttpSerializer serializer) { + final HttpStreamingSerializer serializer) { payloadHolder.payloadBody(payloadBody, serializer); return this; } + @Deprecated @Override public StreamingHttpRequest transformPayloadBody(Function, Publisher> transformer, - HttpSerializer serializer) { + HttpSerializer serializer) { + payloadHolder.transformPayloadBody(bufPub -> + serializer.serialize(headers(), transformer.apply(bufPub), payloadHolder.allocator())); + return this; + } + + @Override + public StreamingHttpRequest transformPayloadBody(final Function, Publisher> transformer, + final HttpStreamingSerializer serializer) { payloadHolder.transformPayloadBody(transformer, serializer); return this; } + @Override + public StreamingHttpRequest transformPayloadBody(final Function, Publisher> transformer, + final HttpStreamingDeserializer deserializer, + final HttpStreamingSerializer serializer) { + return transformPayloadBody(bufPub -> + transformer.apply(deserializer.deserialize(headers(), bufPub, payloadHolder.allocator())), + serializer); + } + @Override public StreamingHttpRequest transformPayloadBody(UnaryOperator> transformer) { payloadHolder.transformPayloadBody(transformer); @@ -187,6 +227,13 @@ public StreamingHttpRequest transform(final TrailersTransformer t return this; } + @Override + public StreamingHttpRequest transform(final TrailersTransformer trailersTransformer, + final HttpStreamingDeserializer deserializer) { + payloadHolder.transform(trailersTransformer, deserializer); + return this; + } + @Override public Single toRequest() { return payloadHolder.aggregate().map(pair -> new DefaultHttpRequest(this, pair.payload, pair.trailers)); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpResponse.java index b22d52bc8b..7e046c346e 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultStreamingHttpResponse.java @@ -50,6 +50,7 @@ public StreamingHttpResponse status(final HttpResponseStatus status) { return this; } + @Deprecated @Override public StreamingHttpResponse encoding(final ContentCodec encoding) { super.encoding(encoding); @@ -61,6 +62,11 @@ public Publisher payloadBody() { return payloadHolder.payloadBody(); } + @Override + public Publisher payloadBody(final HttpStreamingDeserializer deserializer) { + return deserializer.deserialize(headers(), payloadBody(), payloadHolder.allocator()); + } + @Override public Publisher messageBody() { return payloadHolder.messageBody(); @@ -72,20 +78,47 @@ public StreamingHttpResponse payloadBody(final Publisher payloadBody) { return this; } + @Deprecated + @Override + public StreamingHttpResponse payloadBody(final Publisher payloadBody, + final HttpSerializer serializer) { + payloadHolder.transformPayloadBody(bufPub -> + serializer.serialize(headers(), payloadBody, payloadHolder.allocator())); + return this; + } + @Override public StreamingHttpResponse payloadBody(final Publisher payloadBody, - final HttpSerializer serializer) { + final HttpStreamingSerializer serializer) { payloadHolder.payloadBody(payloadBody, serializer); return this; } + @Deprecated @Override public StreamingHttpResponse transformPayloadBody(Function, Publisher> transformer, - HttpSerializer serializer) { + HttpSerializer serializer) { + payloadHolder.transformPayloadBody(bufPub -> + serializer.serialize(headers(), transformer.apply(bufPub), payloadHolder.allocator())); + return this; + } + + @Override + public StreamingHttpResponse transformPayloadBody(final Function, Publisher> transformer, + final HttpStreamingSerializer serializer) { payloadHolder.transformPayloadBody(transformer, serializer); return this; } + @Override + public StreamingHttpResponse transformPayloadBody(final Function, Publisher> transformer, + final HttpStreamingDeserializer deserializer, + final HttpStreamingSerializer serializer) { + return transformPayloadBody(bufPub -> + transformer.apply(deserializer.deserialize(headers(), bufPub, payloadHolder.allocator())), + serializer); + } + @Override public StreamingHttpResponse transformPayloadBody(UnaryOperator> transformer) { payloadHolder.transformPayloadBody(transformer); @@ -104,6 +137,13 @@ public StreamingHttpResponse transform(final TrailersTransformer return this; } + @Override + public StreamingHttpResponse transform(final TrailersTransformer trailersTransformer, + final HttpStreamingDeserializer serializer) { + payloadHolder.transform(trailersTransformer, serializer); + return this; + } + @Override public Single toResponse() { return payloadHolder.aggregate().map(pair -> new DefaultHttpResponse(this, pair.payload, pair.trailers)); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpDeserializer.java index d34326d3bb..264defbc09 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpDeserializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpDeserializer.java @@ -27,10 +27,11 @@ /** * A {@link HttpDeserializer} that can deserialize a {@link TypeHolder} of type {@link T}. - * + * @deprecated Will be removed with {@link HttpDeserializer}. * @param Type to deserialize. * @see DefaultClassHttpDeserializer */ +@Deprecated final class DefaultTypeHttpDeserializer implements HttpDeserializer { private final Serializer serializer; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpSerializer.java index 992f80634c..8d3b40d94f 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpSerializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/DefaultTypeHttpSerializer.java @@ -27,11 +27,12 @@ /** * An {@link HttpSerializer} that serializes a {@link TypeHolder} of type {@link T}. - * + * @deprecated Will be removed with {@link HttpSerializer}. * @param Type to serialize * @see DefaultClassHttpSerializer * @see DefaultSizeAwareTypeHttpSerializer */ +@Deprecated final class DefaultTypeHttpSerializer implements HttpSerializer { private final Consumer addContentType; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FormUrlEncodedSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FormUrlEncodedSerializer.java new file mode 100644 index 0000000000..c30ed42e96 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/FormUrlEncodedSerializer.java @@ -0,0 +1,140 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import javax.annotation.Nullable; + +import static io.servicetalk.http.api.DefaultHttpRequestMetaData.DEFAULT_MAX_QUERY_PARAMS; +import static io.servicetalk.http.api.UriUtils.decodeQueryParams; +import static io.servicetalk.utils.internal.CharsetUtils.standardCharsets; +import static java.util.Collections.emptyMap; + +final class FormUrlEncodedSerializer implements SerializerDeserializer>> { + private static final HashMap CONTINUATIONS_SEPARATORS; + private static final HashMap KEYVALUE_SEPARATORS; + static { + Collection charsets = standardCharsets(); + final int size = charsets.size(); + CONTINUATIONS_SEPARATORS = new HashMap<>(size); + KEYVALUE_SEPARATORS = new HashMap<>(size); + for (Charset charset : charsets) { + final byte[] continuation; + final byte[] keyvalue; + try { + continuation = "&".getBytes(charset); + keyvalue = "=".getBytes(charset); + } catch (Throwable cause) { + continue; + } + CONTINUATIONS_SEPARATORS.put(charset, continuation); + KEYVALUE_SEPARATORS.put(charset, keyvalue); + } + } + + private final Charset charset; + private final byte[] continuationSeparator; + private final byte[] keyValueSeparator; + + FormUrlEncodedSerializer(final Charset charset) { + this.charset = charset; + byte[] continuationSeparator = CONTINUATIONS_SEPARATORS.get(charset); + if (continuationSeparator == null) { + this.continuationSeparator = "&".getBytes(charset); + this.keyValueSeparator = "=".getBytes(charset); + } else { + this.continuationSeparator = continuationSeparator; + this.keyValueSeparator = KEYVALUE_SEPARATORS.get(charset); + } + } + + @Override + public Map> deserialize(final Buffer serializedData, final BufferAllocator allocator) { + return deserialize(serializedData, charset); + } + + @Override + public Buffer serialize(Map> toSerialize, BufferAllocator allocator) { + Buffer buffer = allocator.newBuffer(toSerialize.size() * 10); + serialize(toSerialize, allocator, buffer); + return buffer; + } + + @Override + public void serialize(final Map> toSerialize, final BufferAllocator allocator, + final Buffer buffer) { + // Null values may be omitted + // https://tools.ietf.org/html/rfc1866#section-8.2 + boolean needContinuation = false; + for (Entry> entry : toSerialize.entrySet()) { + String key = entry.getKey(); + if (key == null || key.isEmpty()) { + throw new SerializationException("Null or empty keys are not supported " + + "for x-www-form-urlencoded params"); + } + + List values = entry.getValue(); + if (values != null) { + for (String value : values) { + if (value != null) { + if (needContinuation) { + buffer.writeBytes(continuationSeparator); + } else { + needContinuation = true; + } + buffer.writeBytes(urlEncode(key, charset)); + buffer.writeBytes(keyValueSeparator); + buffer.writeBytes(urlEncode(value, charset)); + } + } + } + } + } + + static Map> deserialize(@Nullable final Buffer buffer, Charset charset) { + if (buffer == null || buffer.readableBytes() == 0) { + return emptyMap(); + } + return decodeQueryParams(buffer.toString(charset), charset, DEFAULT_MAX_QUERY_PARAMS, (value, charset2) -> { + try { + return URLDecoder.decode(value, charset2.name()); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("URLDecoder failed to find Charset: " + charset2, e); + } + }); + } + + private static byte[] urlEncode(final String value, Charset charset) { + try { + return URLEncoder.encode(value, charset.name()).getBytes(charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HeaderUtils.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HeaderUtils.java index 037b529344..749c5ffdb2 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HeaderUtils.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HeaderUtils.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -51,9 +52,9 @@ import static io.servicetalk.http.api.UriUtils.TCHAR_HMASK; import static io.servicetalk.http.api.UriUtils.TCHAR_LMASK; import static io.servicetalk.http.api.UriUtils.isBitSet; +import static io.servicetalk.utils.internal.CharsetUtils.standardCharsets; import static java.lang.Math.min; import static java.lang.System.lineSeparator; -import static java.nio.charset.Charset.availableCharsets; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableMap; @@ -92,8 +93,8 @@ public final class HeaderUtils { private static final Map CHARSET_PATTERNS; static { - CHARSET_PATTERNS = unmodifiableMap(availableCharsets().entrySet().stream() - .collect(toMap(Map.Entry::getValue, e -> compileCharsetRegex(e.getKey())))); + CHARSET_PATTERNS = unmodifiableMap(standardCharsets().stream() + .collect(toMap(Function.identity(), e -> compileCharsetRegex(e.name())))); } private HeaderUtils() { @@ -232,17 +233,11 @@ static boolean hasContentLength(final HttpHeaders headers) { return headers.contains(CONTENT_LENGTH); } - static void addChunkedEncoding(final HttpHeaders headers) { - if (!isTransferEncodingChunked(headers)) { - headers.add(TRANSFER_ENCODING, CHUNKED); - } - } - - static void setContentEncoding(final HttpHeaders headers, CharSequence encoding) { + static void addContentEncoding(final HttpHeaders headers, CharSequence encoding) { // H2 does not support TE / Transfer-Encoding, so we rely in the presentation encoding only. // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 - headers.set(CONTENT_ENCODING, encoding); - headers.set(VARY, CONTENT_ENCODING); + headers.add(CONTENT_ENCODING, encoding); + headers.add(VARY, CONTENT_ENCODING); } static boolean hasContentEncoding(final HttpHeaders headers) { @@ -667,12 +662,13 @@ private HttpCookiePair findNext(CharSequence cookieHeaderValue) { * If the name can not be matched to any of the supported encodings on this endpoint, then * a {@link UnsupportedContentEncodingException} is thrown. * If the matched encoding is {@link Identity#identity()} then this returns {@code null}. - * + * @deprecated Will be removed along with {@link ContentCodec}. * @param headers The headers to read the encoding name from * @param allowedEncodings The supported encodings for this endpoint * @return The {@link ContentCodec} that matches the name or null if matches to identity */ @Nullable + @Deprecated static ContentCodec identifyContentEncodingOrNullIfIdentity( final HttpHeaders headers, final List allowedEncodings) { @@ -751,6 +747,20 @@ static void checkContentType(final HttpHeaders headers, Predicate c } } + /** + * Checks if the provider headers contain a {@code Content-Type} header that satisfies the supplied predicate. + * + * @param headers the {@link HttpHeaders} instance + * @param contentTypePredicate the content type predicate + */ + static void deserializeCheckContentType(final HttpHeaders headers, Predicate contentTypePredicate) { + if (!contentTypePredicate.test(headers)) { + throw new io.servicetalk.serializer.api.SerializationException( + "Unexpected headers, can not deserialize. Headers: " + + headers.toString(DEFAULT_DEBUG_HEADER_FILTER)); + } + } + private static Pattern compileCharsetRegex(String charsetName) { return compile(".*;\\s*charset=\"?" + quote(charsetName) + "\"?\\s*(;.*|$)", CASE_INSENSITIVE); } @@ -759,12 +769,6 @@ private static boolean hasCharset(final CharSequence contentTypeHeader) { return HAS_CHARSET_PATTERN.matcher(contentTypeHeader).matches(); } - private static void validateCookieTokenAndHeaderName0(final CharSequence key) { - for (int i = 0; i < key.length(); ++i) { - validateToken((byte) key.charAt(i)); - } - } - /** * Validate char is valid token character. * diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDataSourceTransformations.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDataSourceTransformations.java index 80da5484c6..95e7d9984f 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDataSourceTransformations.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDataSourceTransformations.java @@ -54,18 +54,32 @@ private HttpDataSourceTransformations() { // no instances } - static final class BridgeFlowControlAndDiscardOperator implements PublisherOperator { - private static final AtomicIntegerFieldUpdater appliedUpdater = - newUpdater(BridgeFlowControlAndDiscardOperator.class, "applied"); + static final class ObjectBridgeFlowControlAndDiscardOperator extends + AbstractBridgeFlowControlAndDiscardOperator { + ObjectBridgeFlowControlAndDiscardOperator(final Publisher discardedPublisher) { + super(discardedPublisher); + } + } + + static final class BridgeFlowControlAndDiscardOperator extends AbstractBridgeFlowControlAndDiscardOperator { + BridgeFlowControlAndDiscardOperator(final Publisher discardedPublisher) { + super(discardedPublisher); + } + } + + private abstract static class AbstractBridgeFlowControlAndDiscardOperator implements PublisherOperator { + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater appliedUpdater = + newUpdater(AbstractBridgeFlowControlAndDiscardOperator.class, "applied"); private volatile int applied; private final Publisher discardedPublisher; - BridgeFlowControlAndDiscardOperator(final Publisher discardedPublisher) { + AbstractBridgeFlowControlAndDiscardOperator(final Publisher discardedPublisher) { this.discardedPublisher = requireNonNull(discardedPublisher); } @Override - public Subscriber apply(final Subscriber subscriber) { + public final Subscriber apply(final Subscriber subscriber) { // We only need to subscribe to and drain contents from original Publisher a single time. return appliedUpdater.compareAndSet(this, 0, 1) ? new BridgeFlowControlAndDiscardSubscriber<>(subscriber, discardedPublisher) : subscriber; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer.java index 8f0bbf8912..9edc5f6a6d 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer.java @@ -21,10 +21,11 @@ /** * A factory to address deserialization concerns for HTTP request/response payload bodies. + * @deprecated Use {@link HttpDeserializer2} or {@link HttpStreamingDeserializer}. * @param The type of objects to deserialize. */ +@Deprecated public interface HttpDeserializer { - /** * Deserialize a single {@link Object} into a {@link T}. * @param headers The {@link HttpHeaders} associated with the {@code payload}. @@ -36,6 +37,7 @@ public interface HttpDeserializer { /** * Deserialize a {@link BlockingIterable} of {@link Object}s into a {@link BlockingIterable} of type {@link T}. + * * @param headers The {@link HttpHeaders} associated with the {@code payload}. * @param payload Provides the {@link Object}s to deserialize. The contents are assumed to be in memory, otherwise * this method may block. @@ -45,6 +47,7 @@ public interface HttpDeserializer { /** * Deserialize a {@link Publisher} of {@link Object}s into a {@link Publisher} of type {@link T}. + * * @param headers The {@link HttpHeaders} associated with the {@code payload}. * @param payload Provides the {@link Object}s to deserialize. * @return a {@link Publisher} of type {@link T} which is the result of the deserialization. diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer2.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer2.java new file mode 100644 index 0000000000..05bde62452 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpDeserializer2.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; + +/** + * A factory to address deserialization concerns for HTTP request/response payload bodies. + * @param The type of objects to deserialize. + */ +public interface HttpDeserializer2 { + /** + * Deserialize a single {@link Object} into a {@link T}. + * @param headers The {@link HttpHeaders} associated with the {@code payload}. + * @param allocator Used to allocate intermediate {@link Buffer}s if required. + * @param payload The {@link Object} to deserialize. The contents are assumed to be in memory, otherwise this method + * may block. + * @return The result of the deserialization. + */ + T deserialize(HttpHeaders headers, BufferAllocator allocator, Buffer payload); +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpHeaderValues.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpHeaderValues.java index 2053da92cd..8daf79efc0 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpHeaderValues.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpHeaderValues.java @@ -153,6 +153,10 @@ public final class HttpHeaderValues { * {@code "text/plain"} */ public static final CharSequence TEXT_PLAIN = newAsciiString("text/plain"); + /** + * {@code "text/plain"} + */ + public static final CharSequence TEXT_PLAIN_US_ASCII = newAsciiString("text/plain; charset=US-ASCII"); /** * {@code "text/plain"} */ diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMessageBodyIterable.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMessageBodyIterable.java new file mode 100644 index 0000000000..9d5e533c82 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMessageBodyIterable.java @@ -0,0 +1,28 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.concurrent.BlockingIterable; + +/** + * {@link Iterable} of the message-body that + * also provides access to the trailers. + * @param The type of the payload body. + */ +public interface HttpMessageBodyIterable extends BlockingIterable { + @Override + HttpMessageBodyIterator iterator(); +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMessageBodyIterator.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMessageBodyIterator.java new file mode 100644 index 0000000000..59e48d3f39 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMessageBodyIterator.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.concurrent.BlockingIterator; + +import javax.annotation.Nullable; + +/** + * {@link Iterable} of the message-body that + * also provides access to the trailers. + * @param The type of payload body. + */ +public interface HttpMessageBodyIterator extends BlockingIterator { + /** + * Get the trailers associated with this message + * body. Will return {@code null} until this {@link BlockingIterator} has been completely consumed and + * {@link #hasNext()} returns {@code false}. Even after consumption this method may still return {@code null} if + * there are no trailers. + * @return the trailer associated with this message + * body. Will return {@code null} until this {@link BlockingIterator} has been completely consumed and + * {@link #hasNext()} returns {@code false}. Even after consumption this method may still return {@code null} if + * there are no trailers. + */ + @Nullable + HttpHeaders trailers(); +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMetaData.java index 8b0ddeaaab..481179211a 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpMetaData.java @@ -15,6 +15,7 @@ */ package io.servicetalk.http.api; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import io.servicetalk.encoding.api.Identity; @@ -58,19 +59,21 @@ public interface HttpMetaData { * * Any encoding passed here, takes precedence. In other words, a compressed response, can * be disabled by passing {@link Identity#identity()}. - * + * @deprecated Use {@link HttpRequestMetaData#contentEncoding(BufferEncoder)}. * @param encoding The {@link ContentCodec} used for the encoding of the payload. * @return {@code this}. * @see Content-Encoding */ + @Deprecated HttpMetaData encoding(ContentCodec encoding); /** * Returns the {@link ContentCodec} used to encode the payload of a request or a response. - * + * @deprecated Use {@link HttpRequestMetaData#contentEncoding()}. * @return The {@link ContentCodec} used for the encoding of the payload. * @see Content-Encoding */ + @Deprecated @Nullable ContentCodec encoding(); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequest.java index f9fd23fdea..7310a3e244 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequest.java @@ -16,6 +16,7 @@ package io.servicetalk.http.api; import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.nio.charset.Charset; @@ -34,15 +35,25 @@ public interface HttpRequest extends HttpRequestMetaData, TrailersHolder { /** * Gets and deserializes the payload body. - * + * @deprecated Use {@link #payloadBody(HttpDeserializer2)}. * @param deserializer The function that deserializes the underlying {@link Object}. * @param The resulting type of the deserialization operation. * @return The results of the deserialization operation. */ + @Deprecated default T payloadBody(HttpDeserializer deserializer) { return deserializer.deserialize(headers(), payloadBody()); } + /** + * Gets and deserializes the payload body. + * + * @param deserializer The function that deserializes the underlying {@link Object}. + * @param The resulting type of the deserialization operation. + * @return The results of the deserialization operation. + */ + T payloadBody(HttpDeserializer2 deserializer); + /** * Returns an {@link HttpRequest} with its underlying payload set to {@code payloadBody}. * @@ -53,14 +64,25 @@ default T payloadBody(HttpDeserializer deserializer) { /** * Returns an {@link HttpRequest} with its underlying payload set to the results of serialization of {@code pojo}. - * + * @deprecated Use {@link #payloadBody(Object, HttpSerializer2)}. * @param pojo The object to serialize. * @param serializer The {@link HttpSerializer} which converts {@code pojo} into bytes. * @param The type of object to serialize. * @return {@code this} */ + @Deprecated HttpRequest payloadBody(T pojo, HttpSerializer serializer); + /** + * Returns an {@link HttpRequest} with its underlying payload set to the results of serialization of {@code pojo}. + * + * @param pojo The object to serialize. + * @param serializer The {@link HttpSerializer2} which converts {@code pojo} into bytes. + * @param The type of object to serialize. + * @return {@code this} + */ + HttpRequest payloadBody(T pojo, HttpSerializer2 serializer); + /** * Translates this {@link HttpRequest} to a {@link StreamingHttpRequest}. * @@ -111,9 +133,13 @@ default T payloadBody(HttpDeserializer deserializer) { @Override HttpRequest version(HttpProtocolVersion version); + @Deprecated @Override HttpRequest encoding(ContentCodec encoding); + @Override + HttpRequest contentEncoding(@Nullable BufferEncoder encoder); + @Override HttpRequest method(HttpRequestMethod method); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequestMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequestMetaData.java index 02a00d1a47..2354ee2c14 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequestMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpRequestMetaData.java @@ -15,6 +15,7 @@ */ package io.servicetalk.http.api; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.transport.api.HostAndPort; import java.nio.charset.Charset; @@ -419,6 +420,25 @@ default boolean hasQueryParameter(final String key) { @Nullable HostAndPort effectiveHostAndPort(); + /** + * Get the {@link BufferEncoder} to use for this request. The value can be used by filters + * (such as {@link ContentEncodingHttpRequesterFilter}) to apply {@link HttpHeaderNames#CONTENT_ENCODING} to the + * request. + * + * @return the {@link BufferEncoder} to use for this request. + */ + @Nullable + BufferEncoder contentEncoding(); + + /** + * Set the {@link BufferEncoder} to use for this request. The value can be used by filters + * (such as {@link ContentEncodingHttpRequesterFilter}) to apply {@link HttpHeaderNames#CONTENT_ENCODING} to the + * request. + * @param encoder {@link BufferEncoder} to use for this request. + * @return {@code this}. + */ + HttpRequestMetaData contentEncoding(@Nullable BufferEncoder encoder); + @Override HttpRequestMetaData version(HttpProtocolVersion version); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpResponse.java index 36de095c78..6061ca586e 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpResponse.java @@ -30,15 +30,25 @@ public interface HttpResponse extends HttpResponseMetaData, TrailersHolder { /** * Gets and deserializes the payload body. - * + * @deprecated Use {@link #payloadBody(HttpDeserializer2)}. * @param deserializer The function that deserializes the underlying {@link Object}. * @param The resulting type of the deserialization operation. * @return The results of the deserialization operation. */ + @Deprecated default T payloadBody(HttpDeserializer deserializer) { return deserializer.deserialize(headers(), payloadBody()); } + /** + * Gets and deserializes the payload body. + * + * @param deserializer The function that deserializes the underlying {@link Object}. + * @param The resulting type of the deserialization operation. + * @return The results of the deserialization operation. + */ + T payloadBody(HttpDeserializer2 deserializer); + /** * Returns an {@link HttpResponse} with its underlying payload set to {@code payloadBody}. * @@ -49,14 +59,25 @@ default T payloadBody(HttpDeserializer deserializer) { /** * Returns an {@link HttpResponse} with its underlying payload set to the results of serialization of {@code pojo}. - * + * @deprecated Use {@link #payloadBody(Object, HttpSerializer2)}. * @param pojo The object to serialize. * @param serializer The {@link HttpSerializer} which converts {@code pojo} into bytes. * @param The type of object to serialize. * @return {@code this} */ + @Deprecated HttpResponse payloadBody(T pojo, HttpSerializer serializer); + /** + * Returns an {@link HttpResponse} with its underlying payload set to the results of serialization of {@code pojo}. + * + * @param pojo The object to serialize. + * @param serializer The {@link HttpSerializer} which converts {@code pojo} into bytes. + * @param The type of object to serialize. + * @return {@code this} + */ + HttpResponse payloadBody(T pojo, HttpSerializer2 serializer); + /** * Translates this {@link HttpResponse} to a {@link StreamingHttpResponse}. * diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProvider.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProvider.java index c23ec027d0..3e6f0cc35f 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProvider.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProvider.java @@ -21,7 +21,10 @@ /** * A provider of {@link HttpSerializer}s and {@link HttpDeserializer}s. + * @deprecated Use {@link HttpSerializers}, {@link HttpSerializer2}, {@link HttpDeserializer2}, + * {@link HttpStreamingSerializer}, and {@link HttpStreamingDeserializer}. */ +@Deprecated public interface HttpSerializationProvider { /** diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProviders.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProviders.java index e1129f1fbb..de6157f2a1 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProviders.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializationProviders.java @@ -19,6 +19,8 @@ import io.servicetalk.serialization.api.SerializationException; import io.servicetalk.serialization.api.SerializationProvider; import io.servicetalk.serialization.api.Serializer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -38,7 +40,9 @@ /** * A factory to create {@link HttpSerializationProvider}s. + * @deprecated Use {@link HttpSerializers}. */ +@Deprecated public final class HttpSerializationProviders { private HttpSerializationProviders() { @@ -48,11 +52,12 @@ private HttpSerializationProviders() { /** * Creates an {@link HttpSerializer} that can serialize a key-values {@link Map}s * with {@link StandardCharsets#UTF_8} {@code Charset} to urlencoded forms. - * + * @deprecated Use {@link HttpSerializers#formUrlEncodedSerializer()}. * @return {@link HttpSerializer} that could serialize key-value {@link Map}. * @see x-www-form-urlencoded specification */ + @Deprecated public static HttpSerializer>> formUrlEncodedSerializer() { return FormUrlEncodedHttpSerializer.UTF8; } @@ -60,12 +65,13 @@ public static HttpSerializer>> formUrlEncodedSerializer /** * Creates an {@link HttpSerializer} that can serialize key-values {@link Map}s with the specified {@link Charset} * to to urlencoded forms. - * + * @deprecated Use {@link HttpSerializers#formUrlEncodedSerializer(Charset)}. * @param charset {@link Charset} for the key-value {@link Map} that will be serialized. * @return {@link HttpSerializer} that could serialize from key-value {@link Map}. * @see x-www-form-urlencoded specification */ + @Deprecated public static HttpSerializer>> formUrlEncodedSerializer(Charset charset) { final CharSequence contentType = newAsciiString(APPLICATION_X_WWW_FORM_URLENCODED + "; charset=" + charset.name()); @@ -75,7 +81,7 @@ public static HttpSerializer>> formUrlEncodedSerializer /** * Creates an {@link HttpSerializer} that can serialize a key-values {@link Map}s with the specified {@link Charset} * to urlencoded forms. - * + * @deprecated Use {@link HttpSerializers#formUrlEncodedSerializer(Charset)}. * @param charset {@link Charset} for the key-value {@link Map} that will be serialized. * @param addContentType A {@link Consumer} that adds relevant headers to the passed {@link HttpHeaders} matching * the serialized payload. Typically, this involves adding a {@link HttpHeaderNames#CONTENT_TYPE} header. @@ -83,6 +89,7 @@ public static HttpSerializer>> formUrlEncodedSerializer * @see x-www-form-urlencoded specification */ + @Deprecated public static HttpSerializer>> formUrlEncodedSerializer( Charset charset, Consumer addContentType) { return new FormUrlEncodedHttpSerializer(charset, addContentType); @@ -91,11 +98,12 @@ public static HttpSerializer>> formUrlEncodedSerializer /** * Creates an {@link HttpDeserializer} that can deserialize key-values {@link Map}s * with {@link StandardCharsets#UTF_8} from urlencoded forms. - * + * @deprecated Use {@link HttpSerializers#formUrlEncodedSerializer()}. * @return {@link HttpDeserializer} that could deserialize a key-values {@link Map}. * @see x-www-form-urlencoded specification */ + @Deprecated public static HttpDeserializer>> formUrlEncodedDeserializer() { return FormUrlEncodedHttpDeserializer.UTF8; } @@ -103,13 +111,14 @@ public static HttpDeserializer>> formUrlEncodedDeserial /** * Creates an {@link HttpDeserializer} that can deserialize key-values {@link Map}s * with {@link StandardCharsets#UTF_8} from urlencoded forms. - * + * @deprecated Use {@link HttpSerializers#formUrlEncodedSerializer(Charset)}. * @param charset {@link Charset} for the key-value {@link Map} that will be deserialized. * deserialized payload. If the validation fails, then deserialization will fail with {@link SerializationException} * @return {@link HttpDeserializer} that could deserialize a key-value {@link Map}. * @see x-www-form-urlencoded specification */ + @Deprecated public static HttpDeserializer>> formUrlEncodedDeserializer(Charset charset) { return formUrlEncodedDeserializer(charset, headers -> hasContentType(headers, APPLICATION_X_WWW_FORM_URLENCODED, charset)); @@ -118,7 +127,7 @@ public static HttpDeserializer>> formUrlEncodedDeserial /** * Creates an {@link HttpDeserializer} that can deserialize key-values {@link Map}s * with {@link StandardCharsets#UTF_8} from urlencoded forms. - * + * @deprecated Use {@link HttpSerializers#formUrlEncodedSerializer(Charset)}. * @param charset {@link Charset} for the key-value {@link Map} that will be deserialized. * @param checkContentType Checks the {@link HttpHeaders} to see if a compatible encoding is found. * deserialized payload. If the validation fails, then deserialization will fail with {@link SerializationException} @@ -126,6 +135,7 @@ public static HttpDeserializer>> formUrlEncodedDeserial * @see x-www-form-urlencoded specification */ + @Deprecated public static HttpDeserializer>> formUrlEncodedDeserializer( Charset charset, Predicate checkContentType) { return new FormUrlEncodedHttpDeserializer(charset, checkContentType); @@ -134,31 +144,52 @@ public static HttpDeserializer>> formUrlEncodedDeserial /** * Creates an {@link HttpSerializer} that can serialize {@link String}s with {@link StandardCharsets#UTF_8} * {@code Charset}. - * + * @deprecated Use {@link HttpSerializers#textSerializerUtf8()} for aggregated. For streaming, use one of the + * following: + *
    + *
  • {@link HttpSerializers#appSerializerUtf8FixLen()}
  • + *
  • {@link HttpSerializers#appSerializerAsciiVarLen()}
  • + *
  • {@link HttpSerializers#stringStreamingSerializer(Charset, Consumer)}
  • + *
* @return {@link HttpSerializer} that could serialize {@link String}. */ + @Deprecated public static HttpSerializer textSerializer() { return UTF8_STRING_SERIALIZER; } /** * Creates an {@link HttpSerializer} that can serialize {@link String}s with the specified {@link Charset}. - * + * @deprecated Use {@link HttpSerializers#textSerializer(Charset)} for aggregated. For streaming, use one of the + * following: + *
    + *
  • {@link HttpSerializers#appSerializerUtf8FixLen()}
  • + *
  • {@link HttpSerializers#appSerializerAsciiVarLen()}
  • + *
  • {@link HttpSerializers#stringStreamingSerializer(Charset, Consumer)}
  • + *
* @param charset {@link Charset} for the {@link String} that will be serialized. * @return {@link HttpSerializer} that could serialize from {@link String}. */ + @Deprecated public static HttpSerializer textSerializer(Charset charset) { return textSerializer(charset, headers -> hasContentType(headers, TEXT_PLAIN, charset)); } /** * Creates an {@link HttpSerializer} that can serialize {@link String}s with the specified {@link Charset}. - * + * @deprecated Use {@link HttpSerializers#textSerializer(Charset)} for aggregated. For streaming, use one of the + * following: + *
    + *
  • {@link HttpSerializers#appSerializerUtf8FixLen()}
  • + *
  • {@link HttpSerializers#appSerializerAsciiVarLen()}
  • + *
  • {@link HttpSerializers#stringStreamingSerializer(Charset, Consumer)}
  • + *
* @param charset {@link Charset} for the {@link String} that will be serialized. * @param addContentType A {@link Consumer} that adds relevant headers to the passed {@link HttpHeaders} matching * the serialized payload. Typically, this involves adding a {@link HttpHeaderNames#CONTENT_TYPE} header. * @return {@link HttpSerializer} that could serialize from {@link String}. */ + @Deprecated public static HttpSerializer textSerializer(Charset charset, Consumer addContentType) { return new HttpStringSerializer(charset, addContentType); } @@ -166,31 +197,61 @@ public static HttpSerializer textSerializer(Charset charset, Consumer + *
  • {@link HttpSerializers#appSerializerUtf8FixLen()}
  • + *
  • {@link HttpSerializers#appSerializerAsciiVarLen()}
  • + *
  • Aggregate the payload (e.g. {@link StreamingHttpRequest#toRequest()}) and use + * {@link HttpSerializers#textSerializer(Charset)} if your payload is text
  • + *
  • {@link HttpSerializers#streamingSerializer(StreamingSerializerDeserializer, Consumer, Predicate)} + * targeted at your {@link HttpHeaderNames#CONTENT_TYPE}
  • + * * @return {@link HttpDeserializer} that could deserialize {@link String}. */ + @Deprecated public static HttpDeserializer textDeserializer() { return UTF_8_STRING_DESERIALIZER; } /** * Creates an {@link HttpDeserializer} that can deserialize {@link String}s with the specified {@link Charset}. - * + * @deprecated Use {@link HttpSerializers#textSerializer(Charset)} for aggregated. For streaming, use one of the + * following: + *
      + *
    • {@link HttpSerializers#appSerializerUtf8FixLen()}
    • + *
    • {@link HttpSerializers#appSerializerAsciiVarLen()}
    • + *
    • Aggregate the payload (e.g. {@link StreamingHttpRequest#toRequest()}) and use + * {@link HttpSerializers#textSerializer(Charset)} if your payload is text
    • + *
    • {@link HttpSerializers#streamingSerializer(StreamingSerializerDeserializer, Consumer, Predicate)} + * targeted at your {@link HttpHeaderNames#CONTENT_TYPE}
    • + *
    * @param charset {@link Charset} for the {@link String} that will be deserialized. * @return {@link HttpDeserializer} that could deserialize {@link String}. */ + @Deprecated public static HttpDeserializer textDeserializer(Charset charset) { return textDeserializer(charset, headers -> hasContentType(headers, TEXT_PLAIN, charset)); } /** * Creates an {@link HttpDeserializer} that can deserialize {@link String}s with the specified {@link Charset}. - * + * @deprecated Use {@link HttpSerializers#textSerializer(Charset)} for aggregated. For streaming, use one of the + * following: + *
      + *
    • {@link HttpSerializers#appSerializerUtf8FixLen()}
    • + *
    • {@link HttpSerializers#appSerializerAsciiVarLen()}
    • + *
    • Aggregate the payload (e.g. {@link StreamingHttpRequest#toRequest()}) and use + * {@link HttpSerializers#textSerializer(Charset)} if your payload is text
    • + *
    • {@link HttpSerializers#streamingSerializer(StreamingSerializerDeserializer, Consumer, Predicate)} + * targeted at your {@link HttpHeaderNames#CONTENT_TYPE}
    • + *
    * @param charset {@link Charset} for the {@link String} that will be deserialized. * @param checkContentType A {@link Predicate} that validates the passed {@link HttpHeaders} as expected for the * deserialized payload. If the validation fails, then deserialization will fail with {@link SerializationException} * @return {@link HttpDeserializer} that could deserialize {@link String}. */ + @Deprecated public static HttpDeserializer textDeserializer(Charset charset, Predicate checkContentType) { return new HttpStringDeserializer(charset, checkContentType); } @@ -202,10 +263,12 @@ public static HttpDeserializer textDeserializer(Charset charset, Predica * For deserialization, it expects a {@link HttpHeaderNames#CONTENT_TYPE} header with value * {@link HttpHeaderValues#APPLICATION_JSON}. If the expected header is not present, then deserialization will fail * with {@link SerializationException}. - * + * @deprecated Use {@link HttpSerializers#jsonSerializer(SerializerDeserializer)} or + * {@link HttpSerializers#jsonStreamingSerializer(StreamingSerializerDeserializer)}. * @param serializer {@link Serializer} that has the capability of serializing/deserializing to/from JSON. * @return {@link HttpSerializationProvider} that has the capability of serializing/deserializing to/from JSON. */ + @Deprecated public static HttpSerializationProvider jsonSerializer(Serializer serializer) { return serializationProvider(serializer, headers -> headers.set(CONTENT_TYPE, APPLICATION_JSON), headers -> hasContentType(headers, APPLICATION_JSON, null)); @@ -218,11 +281,13 @@ public static HttpSerializationProvider jsonSerializer(Serializer serializer) { * For deserialization, it expects a {@link HttpHeaderNames#CONTENT_TYPE} header with value * {@link HttpHeaderValues#APPLICATION_JSON}. If the expected header is not present, then deserialization will fail * with {@link SerializationException}. - * + * @deprecated Use {@link HttpSerializers#jsonSerializer(SerializerDeserializer)} or + * {@link HttpSerializers#jsonStreamingSerializer(StreamingSerializerDeserializer)}. * @param serializationProvider {@link SerializationProvider} that has the capability of serializing/deserializing * to/from JSON. * @return {@link HttpSerializationProvider} that has the capability of serializing/deserializing to/from JSON. */ + @Deprecated public static HttpSerializationProvider jsonSerializer(SerializationProvider serializationProvider) { return jsonSerializer(new DefaultSerializer(serializationProvider)); } @@ -236,7 +301,8 @@ public static HttpSerializationProvider jsonSerializer(SerializationProvider ser * For deserialization, it would validate headers as specified by the passed * {@link Predicate checkContentType predicate}. If the validation fails, then deserialization will fail with * {@link SerializationException}. - * + * @deprecated Use {@link HttpSerializers}, {@link HttpSerializer2}, {@link HttpDeserializer2}, + * {@link HttpStreamingSerializer}, and {@link HttpStreamingDeserializer}. * @param serializer {@link Serializer} that has the capability of serializing/deserializing to/from a desired * content-type. * @param addContentType A {@link Consumer} that adds relevant headers to the passed {@link HttpHeaders} matching @@ -246,6 +312,7 @@ public static HttpSerializationProvider jsonSerializer(SerializationProvider ser * @return {@link HttpSerializationProvider} that has the capability of serializing/deserializing to/from a desired * content-type. */ + @Deprecated public static HttpSerializationProvider serializationProvider(Serializer serializer, Consumer addContentType, Predicate checkContentType) { @@ -261,7 +328,8 @@ public static HttpSerializationProvider serializationProvider(Serializer seriali * For deserialization, it would validate headers as specified by the passed * {@link Predicate checkContentType predicate}. If the validation fails, then deserialization will fail with * {@link SerializationException}. - * + * @deprecated Use {@link HttpSerializers}, {@link HttpSerializer2}, {@link HttpDeserializer2}, + * {@link HttpStreamingSerializer}, and {@link HttpStreamingDeserializer}. * @param serializationProvider {@link SerializationProvider} that has the capability of serializing/deserializing * to/from a desired content-type. * @param addContentType A {@link Consumer} that adds relevant headers to the passed {@link HttpHeaders} matching @@ -272,6 +340,7 @@ public static HttpSerializationProvider serializationProvider(Serializer seriali * @return {@link HttpSerializationProvider} that has the capability of serializing/deserializing to/from a desired * content-type. */ + @Deprecated public static HttpSerializationProvider serializationProvider(SerializationProvider serializationProvider, Consumer addContentType, Predicate checkContentType) { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer.java index 10267cc2e9..ab8b594519 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer.java @@ -22,21 +22,11 @@ /** * A factory to address serialization concerns for HTTP request/response payload bodies. - * + * @deprecated Use {@link HttpSerializer2} or {@link HttpStreamingSerializer}. * @param The type of objects to serialize. */ -public interface HttpSerializer { - /** - * Serialize an object of type {@link T} into a {@link Buffer}. If necessary the {@link HttpHeaders} should be - * updated to indicate the content-type. - * - * @param headers The {@link HttpHeaders} associated with the serialization operation. - * @param value The object to serialize. - * @param allocator The {@link BufferAllocator} used to create the returned {@link Buffer}. - * @return The result of the serialization operation. - */ - Buffer serialize(HttpHeaders headers, T value, BufferAllocator allocator); - +@Deprecated +public interface HttpSerializer extends HttpSerializer2 { /** * Serialize an {@link BlockingIterable} of type {@link T} into an {@link BlockingIterable} of type * {@link Buffer}. If necessary the {@link HttpHeaders} should be updated to indicate the diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer2.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer2.java new file mode 100644 index 0000000000..492c99fb7e --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializer2.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; + +/** + * A factory to address serialization concerns for HTTP request/response payload bodies. + * @param The type of objects to serialize. + */ +@FunctionalInterface +public interface HttpSerializer2 { + /** + * Serialize an object of type {@link T} into a {@link Buffer}. If necessary the {@link HttpHeaders} should be + * updated to indicate the content-type. + * + * @param headers The {@link HttpHeaders} associated with the serialization operation. + * @param value The object to serialize. + * @param allocator The {@link BufferAllocator} used to create the returned {@link Buffer}. + * @return The result of the serialization operation. + */ + Buffer serialize(HttpHeaders headers, T value, BufferAllocator allocator); +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializerDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializerDeserializer.java new file mode 100644 index 0000000000..c2f1ade058 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializerDeserializer.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2021 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.http.api; + +/** + * Both a {@link HttpSerializer2} and {@link HttpDeserializer2}. + * @param The type of objects to serialize/deserialize. + */ +public interface HttpSerializerDeserializer extends HttpSerializer2, HttpDeserializer2 { +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializers.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializers.java new file mode 100644 index 0000000000..fac326dc27 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpSerializers.java @@ -0,0 +1,391 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; +import io.servicetalk.serializer.utils.FixedLengthStreamingSerializer; +import io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; + +import static io.servicetalk.buffer.api.CharSequences.newAsciiString; +import static io.servicetalk.http.api.HeaderUtils.hasContentType; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; +import static io.servicetalk.http.api.HttpHeaderValues.APPLICATION_JSON; +import static io.servicetalk.http.api.HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED; +import static io.servicetalk.http.api.HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED_UTF_8; +import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN; +import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN_US_ASCII; +import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN_UTF_8; +import static io.servicetalk.serializer.utils.StringSerializer.stringSerializer; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Factory for creation of {@link HttpSerializerDeserializer} and {@link HttpStreamingSerializerDeserializer}. + */ +public final class HttpSerializers { + private static final String APPLICATION_TEXT_FIXED_STR = "application/text-fix-int"; + private static final CharSequence APPLICATION_TEXT_FIXED = newAsciiString(APPLICATION_TEXT_FIXED_STR); + private static final CharSequence APPLICATION_TEXT_FIXED_UTF_8 = + newAsciiString(APPLICATION_TEXT_FIXED + "; charset=UTF-8"); + private static final CharSequence APPLICATION_TEXT_FIXED_US_ASCII = + newAsciiString(APPLICATION_TEXT_FIXED + "; charset=US-ASCII"); + private static final String APPLICATION_TEXT_VARINT_STR = "application/text-var-int"; + private static final CharSequence APPLICATION_TEXT_VARINT = newAsciiString(APPLICATION_TEXT_VARINT_STR); + private static final CharSequence APPLICATION_TEXT_VAR_INT_UTF_8 = + newAsciiString(APPLICATION_TEXT_VARINT + "; charset=UTF-8"); + private static final CharSequence APPLICATION_TEXT_VAR_INT_US_ASCII = + newAsciiString(APPLICATION_TEXT_VARINT + "; charset=US-ASCII"); + + private static final HttpSerializerDeserializer>> FORM_ENCODED_UTF_8 = + new DefaultHttpSerializerDeserializer<>(new FormUrlEncodedSerializer(UTF_8), + headers -> headers.set(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED_UTF_8), + headers -> hasContentType(headers, APPLICATION_X_WWW_FORM_URLENCODED, UTF_8)); + private static final HttpSerializerDeserializer TEXT_UTF_8 = + new DefaultHttpSerializerDeserializer<>(stringSerializer(UTF_8), + headers -> headers.set(CONTENT_TYPE, TEXT_PLAIN_UTF_8), + headers -> hasContentType(headers, TEXT_PLAIN, UTF_8)); + private static final HttpSerializerDeserializer TEXT_ASCII = + new DefaultHttpSerializerDeserializer<>(stringSerializer(US_ASCII), + headers -> headers.set(CONTENT_TYPE, TEXT_PLAIN_US_ASCII), + headers -> hasContentType(headers, TEXT_PLAIN, US_ASCII)); + private static final int MAX_BYTES_PER_CHAR_UTF8 = (int) UTF_8.newEncoder().maxBytesPerChar(); + private static final HttpStreamingSerializerDeserializer APP_STREAMING_FIX_LEN_UTF_8 = + streamingSerializer(new FixedLengthStreamingSerializer<>(stringSerializer(UTF_8), + str -> str.length() * MAX_BYTES_PER_CHAR_UTF8), + headers -> headers.set(CONTENT_TYPE, APPLICATION_TEXT_FIXED_UTF_8), + headers -> hasContentType(headers, APPLICATION_TEXT_FIXED, UTF_8)); + private static final HttpStreamingSerializerDeserializer APP_STREAMING_FIX_LEN_ASCII = + streamingSerializer(new FixedLengthStreamingSerializer<>(stringSerializer(US_ASCII), String::length), + headers -> headers.set(CONTENT_TYPE, APPLICATION_TEXT_FIXED_US_ASCII), + headers -> hasContentType(headers, APPLICATION_TEXT_FIXED, US_ASCII)); + private static final HttpStreamingSerializerDeserializer APP_STREAMING_VAR_LEN_UTF_8 = + streamingSerializer(new VarIntLengthStreamingSerializer<>(stringSerializer(UTF_8), + str -> str.length() * MAX_BYTES_PER_CHAR_UTF8), + headers -> headers.set(CONTENT_TYPE, APPLICATION_TEXT_VAR_INT_UTF_8), + headers -> hasContentType(headers, APPLICATION_TEXT_VARINT, UTF_8)); + private static final HttpStreamingSerializerDeserializer APP_STREAMING_VAR_LEN_ASCII = + streamingSerializer(new VarIntLengthStreamingSerializer<>(stringSerializer(US_ASCII), String::length), + headers -> headers.set(CONTENT_TYPE, APPLICATION_TEXT_VAR_INT_US_ASCII), + headers -> hasContentType(headers, APPLICATION_TEXT_VARINT, US_ASCII)); + + private HttpSerializers() { + } + + /** + * Get a {@link HttpSerializerDeserializer} that can serialize a key-values {@link Map}s + * with {@link StandardCharsets#UTF_8} {@link Charset} to urlencoded forms. + * + * @return {@link HttpSerializerDeserializer} that could serialize key-value {@link Map}. + * @see x-www-form-urlencoded specification + */ + public static HttpSerializerDeserializer>> formUrlEncodedSerializer() { + return FORM_ENCODED_UTF_8; + } + + /** + * Get a {@link HttpSerializerDeserializer} that can serialize a key-values {@link Map}s + * with a {@link Charset} to urlencoded forms. + * + * @param charset The {@link Charset} to use for value encoding. + * @return {@link HttpSerializerDeserializer} that could serialize key-value {@link Map}. + * @see x-www-form-urlencoded specification + */ + public static HttpSerializerDeserializer>> formUrlEncodedSerializer(Charset charset) { + if (UTF_8.equals(charset)) { + return FORM_ENCODED_UTF_8; + } + final CharSequence contentType = newAsciiString(APPLICATION_X_WWW_FORM_URLENCODED + "; charset=" + + charset.name()); + return new DefaultHttpSerializerDeserializer<>(new FormUrlEncodedSerializer(charset), + headers -> headers.set(CONTENT_TYPE, contentType), + headers -> hasContentType(headers, APPLICATION_X_WWW_FORM_URLENCODED, charset)); + } + + /** + * Creates an {@link HttpSerializerDeserializer} that can serialize {@link String}s with + * {@link StandardCharsets#UTF_8}. + * + * @return {@link HttpSerializerDeserializer} that can serialize {@link String}s. + */ + public static HttpSerializerDeserializer textSerializerUtf8() { + return TEXT_UTF_8; + } + + /** + * Creates an {@link HttpSerializerDeserializer} that can serialize {@link String}s with + * {@link StandardCharsets#US_ASCII}. + * + * @return {@link HttpSerializerDeserializer} that can serialize {@link String}s. + */ + public static HttpSerializerDeserializer textSerializerAscii() { + return TEXT_ASCII; + } + + /** + * Creates an {@link HttpSerializerDeserializer} that can serialize {@link String}s with + * a {@link Charset}. + * + * @param charset The {@link Charset} to use for encoding. + * @return {@link HttpSerializerDeserializer} that can serialize {@link String}s. + */ + public static HttpSerializerDeserializer textSerializer(Charset charset) { + if (UTF_8.equals(charset)) { + return TEXT_UTF_8; + } else if (US_ASCII.equals(charset)) { + return TEXT_ASCII; + } + final CharSequence contentType = newAsciiString("text/plain; charset=" + charset.name()); + return new DefaultHttpSerializerDeserializer<>(stringSerializer(charset), + headers -> headers.set(CONTENT_TYPE, contentType), + headers -> hasContentType(headers, TEXT_PLAIN, charset)); + } + + /** + * Creates a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#UTF_8} encoding using fixed {@code int} length delimited framing. The framing is required + * so the same {@link String} objects can be deserialized by the peer, otherwise the boundaries aren't known. If + * the desire is to serialize raw data contained in the {@link String}, see + * {@link #stringStreamingSerializer(Charset, Consumer)}. The {@link HttpHeaderNames#CONTENT_TYPE} value prefix is + * {@value #APPLICATION_TEXT_FIXED_STR}. + * + * @return a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#UTF_8} encoding using fixed {@code int} length delimited framing. + * @see FixedLengthStreamingSerializer + */ + public static HttpStreamingSerializerDeserializer appSerializerUtf8FixLen() { + return APP_STREAMING_FIX_LEN_UTF_8; + } + + /** + * Creates a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#UTF_8} encoding using variable {@code int} length delimited framing. The framing is + * required so the same {@link String} objects can be deserialized by the peer, otherwise the boundaries aren't + * known. If the desire is to serialize raw data contained in the {@link String}, see + * {@link #stringStreamingSerializer(Charset, Consumer)}.The {@link HttpHeaderNames#CONTENT_TYPE} value prefix is + * {@value #APPLICATION_TEXT_VARINT_STR}. + * + * @return a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#UTF_8} encoding using variable {@code int} length delimited framing. + * @see VarIntLengthStreamingSerializer + */ + public static HttpStreamingSerializerDeserializer appSerializerUtf8VarLen() { + return APP_STREAMING_VAR_LEN_UTF_8; + } + + /** + * Creates a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#US_ASCII} encoding using fixed {@code int} length delimited framing. The framing is + * required so the same {@link String} objects can be deserialized by the peer, otherwise the boundaries aren't + * known. If the desire is to serialize raw data contained in the {@link String}, see + * {@link #stringStreamingSerializer(Charset, Consumer)}. The {@link HttpHeaderNames#CONTENT_TYPE} value prefix is + * {@value #APPLICATION_TEXT_FIXED_STR}. + * + * @return a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#US_ASCII} encoding using fixed {@code int} length delimited framing. + * @see FixedLengthStreamingSerializer + */ + public static HttpStreamingSerializerDeserializer appSerializerAsciiFixLen() { + return APP_STREAMING_FIX_LEN_ASCII; + } + + /** + * Creates a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#US_ASCII} encoding using variable {@code int} length delimited framing. The framing is + * required so the same {@link String} objects can be deserialized by the peer, otherwise the boundaries aren't + * known. If the desire is to serialize raw data contained in the {@link String}, see + * {@link #stringStreamingSerializer(Charset, Consumer)}. The {@link HttpHeaderNames#CONTENT_TYPE} value prefix is + * {@value #APPLICATION_TEXT_VARINT_STR}. + * + * @return a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@link StandardCharsets#US_ASCII} encoding using variable {@code int} length delimited framing. + * @see VarIntLengthStreamingSerializer + */ + public static HttpStreamingSerializerDeserializer appSerializerAsciiVarLen() { + return APP_STREAMING_VAR_LEN_ASCII; + } + + /** + * Creates a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@code charset} encoding using fixed {@code int} length delimited framing. The framing is required so the same + * {@link String} objects can be deserialized by the peer, otherwise the boundaries aren't known. If the desire is + * to serialize raw data contained in the {@link String}, see {@link #stringStreamingSerializer(Charset, Consumer)}. + * The {@link HttpHeaderNames#CONTENT_TYPE} value prefix is {@value #APPLICATION_TEXT_FIXED_STR}. + * + * @param charset The character encoding to use for serialization. + * @return a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@code charset} encoding using fixed {@code int} length delimited framing. + * @see FixedLengthStreamingSerializer + */ + public static HttpStreamingSerializerDeserializer appSerializerFixLen(Charset charset) { + if (UTF_8.equals(charset)) { + return APP_STREAMING_FIX_LEN_UTF_8; + } else if (US_ASCII.equals(charset)) { + return APP_STREAMING_FIX_LEN_ASCII; + } + final int maxBytesPerChar = (int) charset.newEncoder().maxBytesPerChar(); + CharSequence contentType = newAsciiString(APPLICATION_TEXT_FIXED + "; charset=" + charset.name()); + return streamingSerializer(new FixedLengthStreamingSerializer<>(stringSerializer(charset), + str -> str.length() * maxBytesPerChar), + headers -> headers.set(CONTENT_TYPE, contentType), + headers -> hasContentType(headers, APPLICATION_TEXT_FIXED, charset)); + } + + /** + * Creates a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@code charset} encoding using fixed {@code int} length delimited framing. The framing is required so the same + * {@link String} objects can be deserialized by the peer, otherwise the boundaries aren't known. If the desire is + * to serialize raw data contained in the {@link String}, see {@link #stringStreamingSerializer(Charset, Consumer)}. + * The {@link HttpHeaderNames#CONTENT_TYPE} value prefix is {@value #APPLICATION_TEXT_VARINT_STR}. + * + * @param charset The character encoding to use for serialization. + * @return a {@link HttpStreamingSerializerDeserializer} that serializes {@link String}s with + * {@code charset} encoding using fixed {@code int} length delimited framing. + * @see VarIntLengthStreamingSerializer + */ + public static HttpStreamingSerializerDeserializer appSerializerVarLen(Charset charset) { + if (UTF_8.equals(charset)) { + return APP_STREAMING_VAR_LEN_UTF_8; + } else if (US_ASCII.equals(charset)) { + return APP_STREAMING_VAR_LEN_ASCII; + } + final int maxBytesPerChar = (int) charset.newEncoder().maxBytesPerChar(); + CharSequence contentType = newAsciiString(APPLICATION_TEXT_VARINT + "; charset=" + charset.name()); + return streamingSerializer(new VarIntLengthStreamingSerializer<>(stringSerializer(charset), + str -> str.length() * maxBytesPerChar), + headers -> headers.set(CONTENT_TYPE, contentType), + headers -> hasContentType(headers, APPLICATION_TEXT_VARINT, charset)); + } + + /** + * Create a {@link HttpStreamingSerializer} that serializes {@link String}. This method is useful if the payload + * body is provided in {@link String} and the {@link HttpHeaderNames#CONTENT_TYPE} is known a-prior + * (e.g. streaming raw json data from a stream of {@link String}s). Deserialization should be done using + * the a-prior knowledge to use a compatible {@link HttpStreamingDeserializer}. + * @param charset The character encoding to use for serialization. + * @param headersSerializeConsumer Sets the headers to indicate the appropriate encoding and content type. + * @return a {@link HttpStreamingSerializer} that uses a {@link Serializer} for serialization. + */ + public static HttpStreamingSerializer stringStreamingSerializer( + Charset charset, Consumer headersSerializeConsumer) { + final int maxBytesPerChar = (int) charset.newEncoder().maxBytesPerChar(); + return streamingSerializer(stringSerializer(charset), str -> str.length() * maxBytesPerChar, + headersSerializeConsumer); + } + + /** + * Create a {@link HttpStreamingSerializer} that serializes {@code byte[]}. This method is useful if the payload + * body is provided in {@code byte[]} and the {@link HttpHeaderNames#CONTENT_TYPE} is known a-prior + * (e.g. streaming raw json data from a stream of {@code byte[]}s). Deserialization should be done using + * the a-prior knowledge to use a compatible {@link HttpStreamingDeserializer}. + * @param headersSerializeConsumer Sets the headers to indicate the appropriate encoding and content type. + * @return a {@link HttpStreamingSerializer} that uses a {@link Serializer} for serialization. + */ + public static HttpStreamingSerializer bytesStreamingSerializer( + Consumer headersSerializeConsumer) { + return new DefaultHttpStreamingSerializer<>(NonFramedBytesStreamingSerializer.INSTANCE, + headersSerializeConsumer); + } + + /** + * Creates an {@link HttpSerializerDeserializer} that targets {@link HttpHeaderValues#APPLICATION_JSON}. + * + * @param serializer Used to serialize each {@link T}. + * @param Type of object to serialize. + * @return {@link HttpSerializerDeserializer} that targets {@link HttpHeaderValues#APPLICATION_JSON}. + */ + public static HttpSerializerDeserializer jsonSerializer(SerializerDeserializer serializer) { + return new DefaultHttpSerializerDeserializer<>(serializer, + headers -> headers.set(CONTENT_TYPE, APPLICATION_JSON), + headers -> hasContentType(headers, APPLICATION_JSON, null)); + } + + /** + * Creates an {@link HttpStreamingSerializerDeserializer} that targets {@link HttpHeaderValues#APPLICATION_JSON}. + * + * @param serializer Used to serialize each {@link T}. + * @param Type of object to serialize. + * @return {@link HttpStreamingSerializerDeserializer} that targets {@link HttpHeaderValues#APPLICATION_JSON}. + */ + public static HttpStreamingSerializerDeserializer jsonStreamingSerializer( + StreamingSerializerDeserializer serializer) { + return new DefaultHttpStreamingSerializerDeserializer<>(serializer, + headers -> headers.set(CONTENT_TYPE, APPLICATION_JSON), + headers -> hasContentType(headers, APPLICATION_JSON, null)); + } + + /** + * Creates an {@link HttpSerializerDeserializer} that uses {@link SerializerDeserializer} for serialization. + * + * @param serializer Used to serialize each {@link T}. + * @param headersSerializeConsumer Sets the headers to indicate the appropriate encoding and content type. + * @param headersDeserializePredicate Validates the headers are of the supported encoding and content type. + * @param Type of object to serialize. + * @return {@link HttpSerializerDeserializer} that uses a {@link SerializerDeserializer} for serialization. + */ + public static HttpSerializerDeserializer serializer( + SerializerDeserializer serializer, Consumer headersSerializeConsumer, + Predicate headersDeserializePredicate) { + return new DefaultHttpSerializerDeserializer<>(serializer, headersSerializeConsumer, + headersDeserializePredicate); + } + + /** + * Creates an {@link HttpStreamingSerializerDeserializer} that uses {@link StreamingSerializerDeserializer} for + * serialization. + * + * @param serializer Used to serialize each {@link T}. + * @param headersSerializeConsumer Sets the headers to indicate the appropriate encoding and content type. + * @param headersDeserializePredicate Validates the headers are of the supported encoding and content type. + * @param Type of object to serialize. + * @return {@link HttpStreamingSerializerDeserializer} that uses a {@link StreamingSerializerDeserializer} for + * serialization. + */ + public static HttpStreamingSerializerDeserializer streamingSerializer( + StreamingSerializerDeserializer serializer, Consumer headersSerializeConsumer, + Predicate headersDeserializePredicate) { + return new DefaultHttpStreamingSerializerDeserializer<>(serializer, headersSerializeConsumer, + headersDeserializePredicate); + } + + /** + * Create a {@link HttpStreamingSerializer} that uses a {@link Serializer} for serialization. This method is useful + * if the payload body is provided in non-{@link Buffer} type and the {@link HttpHeaderNames#CONTENT_TYPE} is known + * a-prior (e.g. streaming raw json data from a stream of {@link String}s). Deserialization should be done using + * the a-prior knowledge to use a compatible {@link HttpStreamingDeserializer}. + * @param serializer Used to serialize each {@link T} chunk. + * @param bytesEstimator Provides an estimate of how many bytes to allocate for each {@link Buffer} to serialize to. + * @param headersSerializeConsumer Sets the headers to indicate the appropriate encoding and content type. + * @param Type of object to serialize. + * @return a {@link HttpStreamingSerializer} that uses a {@link Serializer} for serialization. + */ + public static HttpStreamingSerializer streamingSerializer( + Serializer serializer, ToIntFunction bytesEstimator, Consumer headersSerializeConsumer) { + return new DefaultHttpStreamingSerializer<>(serializer, bytesEstimator, headersSerializeConsumer); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingDeserializer.java new file mode 100644 index 0000000000..f8485070c2 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingDeserializer.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; + +/** + * HTTP request/response deserialization for streaming payload bodies. + * @param The type of objects to serialize. + */ +public interface HttpStreamingDeserializer { + /** + * Deserialize a {@link Publisher} of {@link Object}s into a {@link Publisher} of type {@link T}. + * @param headers The {@link HttpHeaders} associated with the {@code payload}. + * @param payload Provides the {@link Object}s to deserialize. + * @param allocator Used to allocate {@link Buffer}s if necessary during deserialization. + * @return a {@link Publisher} of type {@link T} which is the result of the deserialization. + */ + Publisher deserialize(HttpHeaders headers, Publisher payload, BufferAllocator allocator); + + /** + * Deserialize a {@link BlockingIterable} of {@link Object}s into a {@link BlockingIterable} of type {@link T}. + * @param headers The {@link HttpHeaders} associated with the {@code payload}. + * @param payload Provides the {@link Object}s to deserialize. The contents are assumed to be in memory, otherwise + * this method may block. + * @param allocator Used to allocate {@link Buffer}s if necessary during deserialization. + * @return a {@link BlockingIterable} of type {@link T} which is the result of the deserialization. + */ + default BlockingIterable deserialize(HttpHeaders headers, BlockingIterable payload, + BufferAllocator allocator) { + return deserialize(headers, Publisher.fromIterable(payload), allocator).toIterable(); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingSerializer.java new file mode 100644 index 0000000000..8b9acd61ac --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingSerializer.java @@ -0,0 +1,68 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; + +/** + * HTTP request/response serialization for streaming payload bodies. + * @param The type of objects to serialize. + */ +public interface HttpStreamingSerializer { + /** + * Serialize a {@link Publisher} of type {@link T} into a {@link Publisher} of type {@link Buffer}. If necessary the + * {@link HttpHeaders} should be updated to indicate the + * content-type. + * + * @param headers The {@link HttpHeaders} associated with the serialization operation. + * @param value The objects to serialize. + * @param allocator The {@link BufferAllocator} used to create the resulting {@link Buffer}s. + * @return The result of the serialization operation. + */ + Publisher serialize(HttpHeaders headers, Publisher value, BufferAllocator allocator); + + /** + * Serialize an {@link BlockingIterable} of type {@link T} into an {@link BlockingIterable} of type + * {@link Buffer}. If necessary the {@link HttpHeaders} should be updated to indicate the + * content-type. + * + * @param headers The {@link HttpHeaders} associated with the serialization operation. + * @param value The objects to serialize. + * @param allocator The {@link BufferAllocator} used to create the resulting {@link Buffer}s. + * @return The result of the serialization operation. + */ + default BlockingIterable serialize(HttpHeaders headers, BlockingIterable value, + BufferAllocator allocator) { + return serialize(headers, Publisher.fromIterable(value), allocator).toIterable(); + } + + /** + * Returns an {@link HttpPayloadWriter} of type {@link T} which serializes each + * {@link HttpPayloadWriter#write(Object) written object} into a {@link Buffer}. If necessary the + * {@link HttpHeaders} should be updated to indicate the + * content-type. + * + * @param headers The {@link HttpHeaders} associated with the serialization operation. + * @param payloadWriter The {@link HttpPayloadWriter} which writes serialized {@link Buffer}s. + * @param allocator The {@link BufferAllocator} used to create the resulting {@link Buffer}s. + * @return The {@link HttpPayloadWriter} of type {@link T} with embedded serialization into a {@link Buffer}. + */ + HttpPayloadWriter serialize(HttpHeaders headers, HttpPayloadWriter payloadWriter, + BufferAllocator allocator); +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingSerializerDeserializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingSerializerDeserializer.java new file mode 100644 index 0000000000..b76a16c1de --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/HttpStreamingSerializerDeserializer.java @@ -0,0 +1,24 @@ +/* + * Copyright © 2021 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.http.api; + +/** + * Both a {@link HttpStreamingSerializer} and {@link HttpStreamingDeserializer}. + * @param The type of objects to serialize/deserialize. + */ +public interface HttpStreamingSerializerDeserializer + extends HttpStreamingSerializer, HttpStreamingDeserializer { +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/NonFramedBytesStreamingSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/NonFramedBytesStreamingSerializer.java new file mode 100644 index 0000000000..4ce5f1d015 --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/NonFramedBytesStreamingSerializer.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.serializer.api.StreamingSerializer; + +final class NonFramedBytesStreamingSerializer implements StreamingSerializer { + static final StreamingSerializer INSTANCE = new NonFramedBytesStreamingSerializer(); + + private NonFramedBytesStreamingSerializer() { + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize.map(allocator::wrap); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/NonFramedStreamingSerializer.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/NonFramedStreamingSerializer.java new file mode 100644 index 0000000000..42545cc45a --- /dev/null +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/NonFramedStreamingSerializer.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.serializer.api.Serializer; +import io.servicetalk.serializer.api.StreamingSerializer; + +import java.util.function.ToIntFunction; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link StreamingSerializer} where the framing is known a-prior in order to deserialize the data. + * @param The type of objects to serialize. + */ +final class NonFramedStreamingSerializer implements StreamingSerializer { + private final Serializer serializer; + private final ToIntFunction bytesEstimator; + + /** + * Create a new instance. + * @param serializer The {@link Serializer} to use for each chunk of data. + * @param bytesEstimator Provide a size estimate for newly allocated {@link Buffer}s to serialize data into. + */ + NonFramedStreamingSerializer(final Serializer serializer, final ToIntFunction bytesEstimator) { + this.serializer = requireNonNull(serializer); + this.bytesEstimator = requireNonNull(bytesEstimator); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize.map(t -> { + Buffer buffer = allocator.newBuffer(bytesEstimator.applyAsInt(t)); + serializer.serialize(t, allocator, buffer); + return buffer; + }); + } +} diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpPayloadHolder.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpPayloadHolder.java index a65fe1e7a9..22e9f3331a 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpPayloadHolder.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpPayloadHolder.java @@ -26,6 +26,7 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.HttpDataSourceTransformations.BridgeFlowControlAndDiscardOperator; import io.servicetalk.http.api.HttpDataSourceTransformations.HttpTransportBufferFilterOperator; +import io.servicetalk.http.api.HttpDataSourceTransformations.ObjectBridgeFlowControlAndDiscardOperator; import io.servicetalk.http.api.HttpDataSourceTransformations.PayloadAndTrailers; import java.util.Objects; @@ -98,13 +99,24 @@ void payloadBody(final Publisher payloadBody) { } } - void payloadBody(final Publisher payloadBody, final HttpSerializer serializer) { + void messageBody(Publisher msgBody) { + payloadInfo.setEmpty(messageBody == EMPTY); + if (messageBody == null) { + messageBody = requireNonNull(msgBody); + } else { // discard old trailers + messageBody = msgBody.liftSync(new ObjectBridgeFlowControlAndDiscardOperator(messageBody)); + } + payloadInfo.setMayHaveTrailersAndGenericTypeBuffer(true); + } + + void payloadBody(final Publisher payloadBody, final HttpStreamingSerializer serializer) { payloadBody(serializer.serialize(headers, payloadBody, allocator)); // Because #serialize(...) method may apply operators, check the original payloadBody again: payloadInfo.setEmpty(payloadBody == empty()); } - void transformPayloadBody(Function, Publisher> transformer, HttpSerializer serializer) { + void transformPayloadBody(Function, Publisher> transformer, + HttpStreamingSerializer serializer) { transformPayloadBody(bufPub -> serializer.serialize(headers, transformer.apply(bufPub), allocator)); } @@ -133,48 +145,33 @@ void transformMessageBody(UnaryOperator> transformer) { messageBody = transformer.apply(messageBody()); } + void transform(final TrailersTransformer trailersTransformer, + final HttpStreamingDeserializer serializer) { + transform(trailersTransformer, body -> defer(() -> { + final Processor trailersProcessor = newSingleProcessor(); + final Publisher transformedPayloadBody = body.liftSync( + new PreserveTrailersBufferOperator(trailersProcessor)); + return merge(serializer.deserialize(headers, transformedPayloadBody, allocator), + fromSource(trailersProcessor)).scanWith(() -> + new TrailersMapper<>(trailersTransformer, headersFactory)) + .subscribeShareContext(); + })); + } + void transform(final TrailersTransformer trailersTransformer) { + transform(trailersTransformer, + body -> body.scanWith(() -> new TrailersMapper<>(trailersTransformer, headersFactory))); + } + + private void transform(final TrailersTransformer trailersTransformer, + final Function, Publisher> internalTransformer) { if (messageBody == null) { messageBody = defer(() -> - from(trailersTransformer.payloadComplete(trailersTransformer.newState(), - headersFactory.newEmptyTrailers())).subscribeShareContext()); + from(trailersTransformer.payloadComplete(trailersTransformer.newState(), + headersFactory.newEmptyTrailers())).subscribeShareContext()); } else { - payloadInfo.setEmpty(false); // transformer may add payload content - messageBody = messageBody.scanWith(() -> new ScanWithMapper() { - @Nullable - private final T state = trailersTransformer.newState(); - @Nullable - private HttpHeaders trailers; - - @Override - public Object mapOnNext(@Nullable final Object next) { - if (next instanceof HttpHeaders) { - if (trailers != null) { - throwDuplicateTrailersException(trailers, next); - } - trailers = (HttpHeaders) next; - return trailersTransformer.payloadComplete(state, trailers); - } else if (trailers != null) { - throwOnNextAfterTrailersException(trailers, next); - } - return trailersTransformer.accept(state, (Buffer) requireNonNull(next)); - } - - @Override - public Object mapOnError(final Throwable t) throws Throwable { - return trailersTransformer.catchPayloadFailure(state, t, headersFactory.newEmptyTrailers()); - } - - @Override - public Object mapOnComplete() { - return trailersTransformer.payloadComplete(state, headersFactory.newEmptyTrailers()); - } - - @Override - public boolean mapTerminal() { - return trailers == null; - } - }); + payloadInfo.setEmpty(false); // transformer may add payload content + messageBody = internalTransformer.apply(messageBody); } payloadInfo.setMayHaveTrailersAndGenericTypeBuffer(true); } @@ -275,6 +272,53 @@ private static Publisher merge(Publisher p, Single s) { return from(p, s.toPublisher().filter(Objects::nonNull)).flatMapMerge(identity(), 2); } + private static final class TrailersMapper implements ScanWithMapper { + private final TrailersTransformer trailersTransformer; + private final HttpHeadersFactory headersFactory; + @Nullable + private final T state; + @Nullable + private HttpHeaders trailers; + + private TrailersMapper(final TrailersTransformer trailersTransformer, + final HttpHeadersFactory headersFactory) { + this.trailersTransformer = requireNonNull(trailersTransformer); + this.headersFactory = headersFactory; + state = trailersTransformer.newState(); + } + + @Override + public Object mapOnNext(@Nullable final Object next) { + if (next instanceof HttpHeaders) { + if (trailers != null) { + throwDuplicateTrailersException(trailers, next); + } + trailers = (HttpHeaders) next; + return trailersTransformer.payloadComplete(state, trailers); + } else if (trailers != null) { + throwOnNextAfterTrailersException(trailers, next); + } + @SuppressWarnings("unchecked") + final S nextS = (S) requireNonNull(next); + return trailersTransformer.accept(state, nextS); + } + + @Override + public Object mapOnError(final Throwable t) throws Throwable { + return trailersTransformer.catchPayloadFailure(state, t, headersFactory.newEmptyTrailers()); + } + + @Override + public Object mapOnComplete() { + return trailersTransformer.payloadComplete(state, headersFactory.newEmptyTrailers()); + } + + @Override + public boolean mapTerminal() { + return trailers == null; + } + } + private static final class PreserveTrailersBufferOperator implements PublisherOperator { private final Processor trailersProcessor; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequest.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequest.java index 78de9fa10f..28e689081e 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequest.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequest.java @@ -18,6 +18,7 @@ import io.servicetalk.buffer.api.Buffer; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import java.nio.charset.Charset; @@ -37,14 +38,25 @@ public interface StreamingHttpRequest extends HttpRequestMetaData { /** * Gets and deserializes the payload body. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)}. * @param deserializer The function that deserializes the underlying {@link Publisher}. * @param The resulting type of the deserialization operation. * @return The results of the deserialization operation. */ + @Deprecated default Publisher payloadBody(HttpDeserializer deserializer) { return deserializer.deserialize(headers(), payloadBody()); } + /** + * Gets and deserializes the payload body. + * + * @param deserializer The function that deserializes the underlying {@link Publisher}. + * @param The resulting type of the deserialization operation. + * @return The results of the deserialization operation. + */ + Publisher payloadBody(HttpStreamingDeserializer deserializer); + /** * Get the message-body which contains the * payload body concatenated with the trailer (if @@ -78,15 +90,34 @@ default Publisher payloadBody(HttpDeserializer deserializer) { *

    * This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the * combination with the existing {@link Publisher} payload body. + * @deprecated Use {@link #payloadBody(Publisher, HttpStreamingSerializer)}. * @param payloadBody The new payload body, prior to serialization. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated StreamingHttpRequest payloadBody(Publisher payloadBody, HttpSerializer serializer); + /** + * Returns a {@link StreamingHttpRequest} with its underlying payload set to the result of serialization. + *

    + * A best effort will be made to apply back pressure to the existing {@link Publisher} payload body. If this default + * policy is not sufficient you can use {@link #transformPayloadBody(Function, HttpStreamingSerializer)} for more + * fine grain control. + *

    + * This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the + * combination with the existing {@link Publisher} payload body. + * @param payloadBody The new payload body, prior to serialization. + * @param serializer Used to serialize the payload body. + * @param The type of objects to serialize. + * @return {@code this} + */ + StreamingHttpRequest payloadBody(Publisher payloadBody, HttpStreamingSerializer serializer); + /** * Returns a {@link StreamingHttpRequest} with its underlying payload transformed to the result of serialization. + * @deprecated Use {@link #transformPayloadBody(Function, HttpStreamingSerializer)}. * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body * {@link Publisher} will be transformed/consumed or else no more requests may be processed. @@ -94,6 +125,7 @@ default Publisher payloadBody(HttpDeserializer deserializer) { * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated StreamingHttpRequest transformPayloadBody(Function, Publisher> transformer, HttpSerializer serializer); @@ -102,12 +134,26 @@ StreamingHttpRequest transformPayloadBody(Function, Publis * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body * {@link Publisher} will be transformed/consumed or else no more requests may be processed. + * @param serializer Used to serialize the payload body. + * @param The type of objects to serialize. + * @return {@code this} + */ + StreamingHttpRequest transformPayloadBody(Function, Publisher> transformer, + HttpStreamingSerializer serializer); + + /** + * Returns a {@link StreamingHttpRequest} with its underlying payload transformed to the result of serialization. + * @deprecated Use {@link #transformPayloadBody(Function, HttpStreamingDeserializer, HttpStreamingSerializer)}. + * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and + * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body + * {@link Publisher} will be transformed/consumed or else no more requests may be processed. * @param deserializer Used to deserialize the existing payload body. * @param serializer Used to serialize the payload body. * @param The type of objects to deserialize. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated default StreamingHttpRequest transformPayloadBody(Function, Publisher> transformer, HttpDeserializer deserializer, HttpSerializer serializer) { @@ -115,6 +161,21 @@ default StreamingHttpRequest transformPayloadBody(Function, transformer.apply(deserializer.deserialize(headers(), bufferPublisher)), serializer); } + /** + * Returns a {@link StreamingHttpRequest} with its underlying payload transformed to the result of serialization. + * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and + * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body + * {@link Publisher} will be transformed/consumed or else no more requests may be processed. + * @param deserializer Used to deserialize the existing payload body. + * @param serializer Used to serialize the payload body. + * @param The type of objects to deserialize. + * @param The type of objects to serialize. + * @return {@code this} + */ + StreamingHttpRequest transformPayloadBody(Function, Publisher> transformer, + HttpStreamingDeserializer deserializer, + HttpStreamingSerializer serializer); + /** * Returns a {@link StreamingHttpRequest} with its underlying payload transformed to {@link Buffer}s. * @param transformer A {@link UnaryOperator} which take as a parameter the existing payload body {@link Publisher} @@ -148,6 +209,18 @@ default StreamingHttpRequest transformPayloadBody(Function, */ StreamingHttpRequest transform(TrailersTransformer trailersTransformer); + /** + * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to {@link S}s, + * with access to the trailers. + * @param trailersTransformer {@link TrailersTransformer} to use for this transform. + * @param deserializer Used to deserialize the existing payload body. + * @param The type of state used during the transformation. + * @param The type of objects to deserialize. + * @return {@code this} + */ + StreamingHttpRequest transform(TrailersTransformer trailersTransformer, + HttpStreamingDeserializer deserializer); + /** * Translates this {@link StreamingHttpRequest} to a {@link HttpRequest}. * @return a {@link Single} that completes with a {@link HttpRequest} representation of this @@ -200,9 +273,13 @@ default StreamingHttpRequest transformPayloadBody(Function, @Override StreamingHttpRequest method(HttpRequestMethod method); + @Deprecated @Override StreamingHttpRequest encoding(ContentCodec encoding); + @Override + StreamingHttpRequest contentEncoding(@Nullable BufferEncoder encoder); + @Override StreamingHttpRequest requestTarget(String requestTarget); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequests.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequests.java index 53b99966d4..5ccd74f6bd 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequests.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpRequests.java @@ -44,7 +44,7 @@ private StreamingHttpRequests() { public static StreamingHttpRequest newRequest( final HttpRequestMethod method, final String requestTarget, final HttpProtocolVersion version, final HttpHeaders headers, final BufferAllocator allocator, final HttpHeadersFactory headersFactory) { - return new DefaultStreamingHttpRequest(method, requestTarget, version, headers, null, allocator, null, + return new DefaultStreamingHttpRequest(method, requestTarget, version, headers, null, null, allocator, null, forUserCreated(), headersFactory); } @@ -70,7 +70,7 @@ public static StreamingHttpRequest newTransportRequest( final HttpRequestMethod method, final String requestTarget, final HttpProtocolVersion version, final HttpHeaders headers, final BufferAllocator allocator, final Publisher payload, final boolean requireTrailerHeader, final HttpHeadersFactory headersFactory) { - return new DefaultStreamingHttpRequest(method, requestTarget, version, headers, null, allocator, payload, + return new DefaultStreamingHttpRequest(method, requestTarget, version, headers, null, null, allocator, payload, forTransportReceive(requireTrailerHeader, version, headers), headersFactory); } } diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpResponse.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpResponse.java index 2f633dbb02..0bda53e9ce 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpResponse.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/StreamingHttpResponse.java @@ -36,14 +36,25 @@ public interface StreamingHttpResponse extends HttpResponseMetaData { /** * Gets and deserializes the payload body. + * @deprecated Use {@link #payloadBody(HttpStreamingDeserializer)}. * @param deserializer The function that deserializes the underlying {@link Publisher}. * @param The resulting type of the deserialization operation. * @return The results of the deserialization operation. */ + @Deprecated default Publisher payloadBody(HttpDeserializer deserializer) { return deserializer.deserialize(headers(), payloadBody()); } + /** + * Gets and deserializes the payload body. + * + * @param deserializer The function that deserializes the underlying {@link Publisher}. + * @param The resulting type of the deserialization operation. + * @return The results of the deserialization operation. + */ + Publisher payloadBody(HttpStreamingDeserializer deserializer); + /** * Get the message-body which contains the * payload body concatenated with the trailer (if @@ -77,15 +88,34 @@ default Publisher payloadBody(HttpDeserializer deserializer) { *

    * This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the * combination with the existing {@link Publisher} payload body. + * @deprecated Use {@link #payloadBody(Publisher, HttpStreamingSerializer)}. * @param payloadBody The new payload body, prior to serialization. * @param serializer Used to serialize the payload body. * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated StreamingHttpResponse payloadBody(Publisher payloadBody, HttpSerializer serializer); + /** + * Returns a {@link StreamingHttpResponse} with its underlying payload set to the result of serialization. + *

    + * A best effort will be made to apply back pressure to the existing {@link Publisher} payload body. If this default + * policy is not sufficient you can use {@link #transformPayloadBody(Function, HttpStreamingSerializer)} for more + * fine grain control. + *

    + * This method reserves the right to delay completion/consumption of {@code payloadBody}. This may occur due to the + * combination with the existing {@link Publisher} payload body. + * @param payloadBody The new payload body, prior to serialization. + * @param serializer Used to serialize the payload body. + * @param The type of objects to serialize. + * @return {@code this} + */ + StreamingHttpResponse payloadBody(Publisher payloadBody, HttpStreamingSerializer serializer); + /** * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to the result of serialization. + * @deprecated Use {@link #transformPayloadBody(Function, HttpStreamingSerializer)}. * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body * {@link Publisher} will be transformed/consumed or else no more responses may be processed. @@ -93,6 +123,7 @@ default Publisher payloadBody(HttpDeserializer deserializer) { * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated StreamingHttpResponse transformPayloadBody(Function, Publisher> transformer, HttpSerializer serializer); @@ -100,6 +131,19 @@ StreamingHttpResponse transformPayloadBody(Function, Publi * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to the result of serialization. * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body + * {@link Publisher} will be transformed/consumed or else no more responses may be processed. + * @param serializer Used to serialize the payload body. + * @param The type of objects to serialize. + * @return {@code this} + */ + StreamingHttpResponse transformPayloadBody(Function, Publisher> transformer, + HttpStreamingSerializer serializer); + + /** + * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to the result of serialization. + * @deprecated Use {@link #transformPayloadBody(Function, HttpStreamingDeserializer, HttpStreamingSerializer)}. + * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and + * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body * {@link Publisher} will be transformed/consumed or else no more requests may be processed. * @param deserializer Used to deserialize the existing payload body. * @param serializer Used to serialize the payload body. @@ -107,6 +151,7 @@ StreamingHttpResponse transformPayloadBody(Function, Publi * @param The type of objects to serialize. * @return {@code this} */ + @Deprecated default StreamingHttpResponse transformPayloadBody(Function, Publisher> transformer, HttpDeserializer deserializer, HttpSerializer serializer) { @@ -114,6 +159,21 @@ default StreamingHttpResponse transformPayloadBody(Function, transformer.apply(deserializer.deserialize(headers(), bufferPublisher)), serializer); } + /** + * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to the result of serialization. + * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and + * returns the new payload body {@link Publisher} prior to serialization. It is assumed the existing payload body + * {@link Publisher} will be transformed/consumed or else no more requests may be processed. + * @param deserializer Used to deserialize the existing payload body. + * @param serializer Used to serialize the payload body. + * @param The type of objects to deserialize. + * @param The type of objects to serialize. + * @return {@code this} + */ + StreamingHttpResponse transformPayloadBody(Function, Publisher> transformer, + HttpStreamingDeserializer deserializer, + HttpStreamingSerializer serializer); + /** * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to {@link Buffer}s. * @param transformer A {@link Function} which take as a parameter the existing payload body {@link Publisher} and @@ -147,6 +207,18 @@ default StreamingHttpResponse transformPayloadBody(Function, */ StreamingHttpResponse transform(TrailersTransformer trailersTransformer); + /** + * Returns a {@link StreamingHttpResponse} with its underlying payload transformed to {@link S}s, + * with access to the trailers. + * @param trailersTransformer {@link TrailersTransformer} to use for this transform. + * @param deserializer Used to deserialize the existing payload body. + * @param The type of state used during the transformation. + * @param The type of objects to deserialize. + * @return {@code this} + */ + StreamingHttpResponse transform(TrailersTransformer trailersTransformer, + HttpStreamingDeserializer deserializer); + /** * Translates this {@link StreamingHttpResponse} to a {@link HttpResponse}. * @return a {@link Single} that completes with a {@link HttpResponse} representation of this @@ -163,6 +235,7 @@ default StreamingHttpResponse transformPayloadBody(Function, @Override StreamingHttpResponse version(HttpProtocolVersion version); + @Deprecated @Override StreamingHttpResponse encoding(ContentCodec encoding); diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedContentEncodingException.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedContentEncodingException.java index 96acaad42b..5fb560e6bf 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedContentEncodingException.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedContentEncodingException.java @@ -15,10 +15,12 @@ */ package io.servicetalk.http.api; +import io.servicetalk.serializer.api.SerializationException; + /** * Exception thrown when a payload was encoded with an unsupported encoder. */ -final class UnsupportedContentEncodingException extends RuntimeException { +final class UnsupportedContentEncodingException extends SerializationException { private static final long serialVersionUID = 5645078707423180235L; diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedHttpChunkException.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedHttpChunkException.java index cf60e94e9f..c1a4f07f05 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedHttpChunkException.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/UnsupportedHttpChunkException.java @@ -15,9 +15,11 @@ */ package io.servicetalk.http.api; +import io.servicetalk.serializer.api.SerializationException; + import javax.annotation.Nullable; -final class UnsupportedHttpChunkException extends IllegalArgumentException { +final class UnsupportedHttpChunkException extends SerializationException { private static final long serialVersionUID = -4336685587984151152L; UnsupportedHttpChunkException(@Nullable Object o) { diff --git a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/BlockingStreamingToStreamingServiceTest.java b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/BlockingStreamingToStreamingServiceTest.java index 1a827381fc..cf8969d842 100644 --- a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/BlockingStreamingToStreamingServiceTest.java +++ b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/BlockingStreamingToStreamingServiceTest.java @@ -56,10 +56,9 @@ import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpResponseStatus.NO_CONTENT; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.utils.internal.PlatformDependent.throwException; -import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -97,7 +96,7 @@ void defaultResponseStatusNoPayload() throws Exception { List response = invokeService(syncService, reqRespFactory.get("/")); assertMetaData(OK, response); - assertPayloadBody("", response); + assertPayloadBody("", response, false); assertEmptyTrailers(response); } @@ -108,7 +107,7 @@ void customResponseStatusNoPayload() throws Exception { List response = invokeService(syncService, reqRespFactory.get("/")); assertMetaData(NO_CONTENT, response); - assertPayloadBody("", response); + assertPayloadBody("", response, false); assertEmptyTrailers(response); } @@ -116,14 +115,15 @@ void customResponseStatusNoPayload() throws Exception { void receivePayloadBody() throws Exception { StringBuilder receivedPayload = new StringBuilder(); BlockingStreamingHttpService syncService = (ctx, request, response) -> { - request.payloadBody().forEach(chunk -> receivedPayload.append(chunk.toString(US_ASCII))); + request.payloadBody().forEach(chunk -> receivedPayload.append(chunk.toString( + chunk.readerIndex() + 4, chunk.readableBytes() - 4, UTF_8))); response.sendMetaData().close(); }; List response = invokeService(syncService, reqRespFactory.post("/") - .payloadBody(from("Hello\n", "World\n"), textSerializer())); + .payloadBody(from("Hello\n", "World\n"), appSerializerUtf8FixLen())); assertMetaData(OK, response); - assertPayloadBody("", response); + assertPayloadBody("", response, true); assertEmptyTrailers(response); assertThat(receivedPayload.toString(), is(HELLO_WORLD)); @@ -140,7 +140,7 @@ void respondWithPayloadBody() throws Exception { List response = invokeService(syncService, reqRespFactory.get("/")); assertMetaData(OK, response); - assertPayloadBody(HELLO_WORLD, response); + assertPayloadBody(HELLO_WORLD, response, false); assertEmptyTrailers(response); } @@ -167,9 +167,9 @@ void echoServiceUsingPayloadWriterWithTrailers() throws Exception { void echoServiceUsingPayloadWriterWithSerializerWithTrailers() throws Exception { echoService((ctx, request, response) -> { response.setHeader(TRAILER, X_TOTAL_LENGTH); - try (HttpPayloadWriter pw = response.sendMetaData(textSerializer())) { + try (HttpPayloadWriter pw = response.sendMetaData(appSerializerUtf8FixLen())) { AtomicInteger totalLength = new AtomicInteger(); - request.payloadBody(textDeserializer()).forEach(chunk -> { + request.payloadBody(appSerializerUtf8FixLen()).forEach(chunk -> { try { totalLength.addAndGet(chunk.length()); pw.write(chunk); @@ -177,7 +177,7 @@ void echoServiceUsingPayloadWriterWithSerializerWithTrailers() throws Exception throwException(e); } }); - pw.setTrailer(X_TOTAL_LENGTH, totalLength.toString()); + pw.setTrailer(X_TOTAL_LENGTH, String.valueOf(addFixedLengthFramingOverhead(totalLength.get(), 2))); } }); } @@ -201,11 +201,11 @@ void echoServiceUsingInputOutputStreamWithTrailers() throws Exception { private void echoService(BlockingStreamingHttpService syncService) throws Exception { List response = invokeService(syncService, reqRespFactory.post("/") - .payloadBody(from("Hello\n", "World\n"), textSerializer())); + .payloadBody(from("Hello\n", "World\n"), appSerializerUtf8FixLen())); assertMetaData(OK, response); assertHeader(TRAILER, X_TOTAL_LENGTH, response); - assertPayloadBody(HELLO_WORLD, response); - assertTrailer(X_TOTAL_LENGTH, String.valueOf(HELLO_WORLD.length()), response); + assertPayloadBody(HELLO_WORLD, response, true); + assertTrailer(X_TOTAL_LENGTH, String.valueOf(addFixedLengthFramingOverhead(HELLO_WORLD.length(), 2)), response); } @Test @@ -471,11 +471,38 @@ private static void assertHeader(CharSequence expectedHeader, CharSequence expec assertThat(metaData.headers().contains(expectedHeader, expectedValue), is(true)); } - private static void assertPayloadBody(String expectedPayloadBody, List response) { - String payloadBody = response.stream() - .filter(obj -> obj instanceof Buffer) - .map(obj -> ((Buffer) obj).toString(US_ASCII)) - .collect(Collectors.joining()); + private static void assertPayloadBody(String expectedPayloadBody, List response, + boolean stripFixedLength) { + String payloadBody; + if (stripFixedLength) { + StringBuilder sb = new StringBuilder(); + Buffer aggregate = DEFAULT_ALLOCATOR.newBuffer(); + int toRead = -1; + for (Object o : response) { + if (o instanceof Buffer) { + aggregate.writeBytes(((Buffer) o)); + while (aggregate.readableBytes() >= toRead) { + if (toRead < 0) { + if (aggregate.readableBytes() >= 4) { + toRead = aggregate.readInt(); + } else { + break; + } + } + if (aggregate.readableBytes() >= toRead) { + sb.append(aggregate.readBytes(toRead).toString(UTF_8)); + toRead = -1; + } + } + } + } + payloadBody = sb.toString(); + } else { + payloadBody = response.stream() + .filter(obj -> obj instanceof Buffer) + .map(obj -> ((Buffer) obj).toString(UTF_8)) + .collect(Collectors.joining()); + } assertThat(payloadBody, is(expectedPayloadBody)); } @@ -492,4 +519,8 @@ private static void assertTrailer(CharSequence expectedTrailer, CharSequence exp assertThat(trailers, is(notNullValue())); assertThat(trailers.contains(expectedTrailer, expectedValue), is(true)); } + + private static int addFixedLengthFramingOverhead(int length, int chunks) { + return length == 0 ? 0 : length + Integer.BYTES * chunks; + } } diff --git a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/ContentEncodingHttpServiceFilterTest.java b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/ContentEncodingHttpServiceFilterTest.java new file mode 100644 index 0000000000..28a5025f14 --- /dev/null +++ b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/ContentEncodingHttpServiceFilterTest.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2021 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.http.api; + +import io.servicetalk.buffer.api.CharSequences; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase; +import static io.servicetalk.http.api.ContentEncodingHttpServiceFilter.matchAndRemoveEncoding; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_ENCODING; +import static java.util.function.Function.identity; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +class ContentEncodingHttpServiceFilterTest { + @Test + void testMatchAndRemoveEncodingFirst() { + List supportedDecoders = new ArrayList<>(); + supportedDecoders.add("foo"); + supportedDecoders.add("deflate"); + HttpHeaders headers = DefaultHttpHeadersFactory.INSTANCE.newHeaders(); + headers.add(CONTENT_ENCODING, CharSequences.newAsciiString(" deflate , gzip ")); + CharSequence match = matchAndRemoveEncoding(supportedDecoders, identity(), + headers.valuesIterator(CONTENT_ENCODING), headers); + assertThat("unexpected match: " + match, contentEqualsIgnoreCase(match, "deflate"), is(true)); + CharSequence contentEncoding = headers.get(CONTENT_ENCODING); + assertThat("unexpected header: " + contentEncoding, contentEqualsIgnoreCase(contentEncoding, " gzip "), + is(true)); + } + + @Test + void testMatchAndRemoveEncodingLastDoesNotMatch() { + List supportedDecoders = new ArrayList<>(); + supportedDecoders.add("gzip"); + HttpHeaders headers = DefaultHttpHeadersFactory.INSTANCE.newHeaders(); + headers.add(CONTENT_ENCODING, CharSequences.newAsciiString(" deflate , gzip ")); + CharSequence match = matchAndRemoveEncoding(supportedDecoders, identity(), + headers.valuesIterator(CONTENT_ENCODING), headers); + assertThat("unexpected match: " + match, match, nullValue()); + CharSequence contentEncoding = headers.get(CONTENT_ENCODING); + assertThat("unexpected header: " + contentEncoding, + contentEqualsIgnoreCase(contentEncoding, " deflate , gzip "), is(true)); + } + + @Test + void testMatchAndRemoveEncodingMiddleDoesNotMatch() { + List supportedDecoders = new ArrayList<>(); + supportedDecoders.add("foo"); + HttpHeaders headers = DefaultHttpHeadersFactory.INSTANCE.newHeaders(); + headers.add(CONTENT_ENCODING, CharSequences.newAsciiString(" deflate , foo , gzip ")); + CharSequence match = matchAndRemoveEncoding(supportedDecoders, identity(), + headers.valuesIterator(CONTENT_ENCODING), headers); + assertThat("unexpected match: " + match, match, nullValue()); + CharSequence contentEncoding = headers.get(CONTENT_ENCODING); + assertThat("unexpected header: " + contentEncoding, + contentEqualsIgnoreCase(contentEncoding, " deflate , foo , gzip "), is(true)); + } +} diff --git a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/RequestConversionTests.java b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/RequestConversionTests.java index 0228cd364e..4fdcdc440d 100644 --- a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/RequestConversionTests.java +++ b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/RequestConversionTests.java @@ -57,7 +57,7 @@ private static Arguments newArguments(final Supplier payload final String paramName) { DefaultPayloadInfo payloadInfo = payloadInfoSupplier.get(); return Arguments.of((Supplier) () -> new DefaultStreamingHttpRequest(GET, "/", HTTP_1_1, - DefaultHttpHeadersFactory.INSTANCE.newHeaders(), identity(), DEFAULT_ALLOCATOR, + DefaultHttpHeadersFactory.INSTANCE.newHeaders(), identity(), null, DEFAULT_ALLOCATOR, new SingleSubscribePublisher(payloadInfo), payloadInfo, DefaultHttpHeadersFactory.INSTANCE), (Supplier) () -> payloadInfo, paramName); } diff --git a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/StreamingHttpPayloadHolderTest.java b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/StreamingHttpPayloadHolderTest.java index 48fc59514c..f1eead9230 100644 --- a/servicetalk-http-api/src/test/java/io/servicetalk/http/api/StreamingHttpPayloadHolderTest.java +++ b/servicetalk-http-api/src/test/java/io/servicetalk/http/api/StreamingHttpPayloadHolderTest.java @@ -16,9 +16,12 @@ package io.servicetalk.http.api; import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.TestPublisher; import io.servicetalk.concurrent.test.internal.TestPublisherSubscriber; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; +import io.servicetalk.serializer.utils.FixedLengthStreamingSerializer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; @@ -36,18 +39,15 @@ import static io.servicetalk.concurrent.api.SourceAdapters.toSource; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; import static io.servicetalk.http.api.DefaultPayloadInfo.forTransportReceive; -import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; import static io.servicetalk.http.api.HttpHeaderNames.TRANSFER_ENCODING; import static io.servicetalk.http.api.HttpHeaderValues.CHUNKED; -import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN_UTF_8; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; -import static java.nio.charset.Charset.defaultCharset; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.serializer.utils.StringSerializer.stringSerializer; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyIterator; import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; @@ -78,6 +78,8 @@ private enum SourceType { Trailers } + private static final StreamingSerializerDeserializer UTF8_DESERIALIZER = + new FixedLengthStreamingSerializer<>(stringSerializer(UTF_8), String::length); private HttpHeaders headers; private HttpHeadersFactory headersFactory; @@ -85,7 +87,6 @@ private enum SourceType { private TestPublisher payloadSource; private final TestPublisher updatedPayloadSource = new TestPublisher<>(); private final TestPublisherSubscriber bufferPayloadSubscriber = new TestPublisherSubscriber<>(); - private final TestPublisherSubscriber stringPayloadSubscriber = new TestPublisherSubscriber<>(); private final TestPublisherSubscriber payloadAndTrailersSubscriber = new TestPublisherSubscriber<>(); private final TransformFunctions transformFunctions = new TransformFunctions(); private final TransformFunctions secondTransformFunctions = new TransformFunctions(); @@ -126,8 +127,8 @@ private void setUp(SourceType sourceType, UpdateMode updateMode, boolean doubleT assertThat(payloadHolder.mayHaveTrailers(), is(sourceTypeTrailers)); break; case SetWithSerializer: - payloadHolder.payloadBody(updatedPayloadSource.map(b -> ((Buffer) b).toString(defaultCharset())), - textSerializer()); + payloadHolder.payloadBody(updatedPayloadSource.map(b -> ((Buffer) b).toString(UTF_8)), + appSerializerUtf8FixLen()); assertThat(payloadHolder.isGenericTypeBuffer(), is(not(sourceTypeTrailers))); assertThat(payloadHolder.mayHaveTrailers(), is(sourceTypeTrailers)); break; @@ -172,7 +173,7 @@ void tearDown() { @ParameterizedTest(name = "{displayName} {index}: source type: {0}, update mode = {1}, double transform? {2}") @MethodSource("data") - void getPayload(SourceType sourceType, UpdateMode updateMode, boolean doubleTransform) { + void getPayload(SourceType sourceType, UpdateMode updateMode, boolean doubleTransform) throws Exception { setUp(sourceType, updateMode, doubleTransform); Publisher payload = payloadHolder.payloadBody(); toSource(payload).subscribe(bufferPayloadSubscriber); @@ -182,18 +183,7 @@ void getPayload(SourceType sourceType, UpdateMode updateMode, boolean doubleTran @ParameterizedTest(name = "{displayName} {index}: source type: {0}, update mode = {1}, double transform? {2}") @MethodSource("data") - void getPayloadWithSerializer(SourceType sourceType, UpdateMode updateMode, boolean doubleTransform) { - setUp(sourceType, updateMode, doubleTransform); - when(headers.get(CONTENT_TYPE)).thenReturn(TEXT_PLAIN_UTF_8); - Publisher payload = textDeserializer().deserialize(headers, payloadHolder.payloadBody()); - toSource(payload).subscribe(stringPayloadSubscriber); - simulateAndVerifyPayloadRead(stringPayloadSubscriber); - simulateAndVerifyPayloadComplete(stringPayloadSubscriber); - } - - @ParameterizedTest(name = "{displayName} {index}: source type: {0}, update mode = {1}, double transform? {2}") - @MethodSource("data") - void getMessageBody(SourceType sourceType, UpdateMode updateMode, boolean doubleTransform) { + void getMessageBody(SourceType sourceType, UpdateMode updateMode, boolean doubleTransform) throws Exception { setUp(sourceType, updateMode, doubleTransform); Publisher bodyAndTrailers = payloadHolder.messageBody(); toSource(bodyAndTrailers).subscribe(payloadAndTrailersSubscriber); @@ -205,7 +195,7 @@ void getMessageBody(SourceType sourceType, UpdateMode updateMode, boolean double @MethodSource("data") void sourceEmitsTrailersUnconditionally(SourceType sourceType, UpdateMode updateMode, - boolean doubleTransform) { + boolean doubleTransform) throws Exception { setUp(sourceType, updateMode, doubleTransform); checkSkipTest(() -> { assumeTrue(sourceType != SourceType.None, () -> "Ignored source type: " + sourceType); @@ -299,16 +289,30 @@ void transformedWithTrailersPayloadEmitsErrorAndSwallowed(SourceType sourceType, } } - private void simulateAndVerifyPayloadRead(final TestPublisherSubscriber subscriber) { + private void simulateAndVerifyPayloadRead(final TestPublisherSubscriber subscriber) throws Exception { if (!canControlPayload()) { return; } + final BufferAllocator alloc = payloadHolder.allocator(); Buffer buf = DEFAULT_ALLOCATOR.fromAscii("foo"); subscriber.awaitSubscription().request(1); getPayloadSource().onNext(buf); - assertThat("Unexpected payload", subscriber.takeOnNext(1), - contains((subscriber == bufferPayloadSubscriber || subscriber == payloadAndTrailersSubscriber) ? - buf : "foo")); + Object rawActual = subscriber.takeOnNext(1).get(0); + + String actual; + if (updateMode == UpdateMode.SetWithSerializer || updateMode == UpdateMode.TransformWithSerializer) { + actual = UTF8_DESERIALIZER.deserialize(Publisher.from((Buffer) rawActual), alloc) + .firstOrError().toFuture().get(); + if (doubleTransform) { + actual = UTF8_DESERIALIZER.deserialize(Publisher.from(alloc.fromUtf8(actual)), alloc) + .firstOrError().toFuture().get(); + } + } else if (subscriber == bufferPayloadSubscriber || subscriber == payloadAndTrailersSubscriber) { + actual = ((Buffer) rawActual).toString(UTF_8); + } else { + actual = rawActual.toString(); + } + assertThat("Unexpected payload", actual, is("foo")); } private void simulateAndVerifyTrailerReadIfApplicable() { @@ -436,7 +440,7 @@ private static final class TransformFunctions { when(transformer.apply(any())).thenAnswer(invocation -> invocation.getArgument(0)); when(stringTransformer.apply(any())).thenAnswer(invocation -> ((Publisher) invocation.getArgument(0)) - .map(buffer -> buffer.toString(defaultCharset()))); + .map(buffer -> buffer.toString(UTF_8))); when(trailerTransformer.accept(any(), any())).thenAnswer(invocation -> invocation.getArgument(1)); } @@ -461,7 +465,7 @@ void setupFor(UpdateMode updateMode, StreamingHttpPayloadHolder payloadHolder, b assertThat(payloadHolder.isGenericTypeBuffer(), is(false)); break; case TransformWithSerializer: - payloadHolder.transformPayloadBody(stringTransformer, textSerializer()); + payloadHolder.transformPayloadBody(stringTransformer, appSerializerUtf8FixLen()); assertThat(payloadHolder.isGenericTypeBuffer(), is(not(sourceTypeTrailers))); assertThat(payloadHolder.mayHaveTrailers(), is(sourceTypeTrailers)); break; diff --git a/servicetalk-http-netty/build.gradle b/servicetalk-http-netty/build.gradle index 506518a1f7..4004ba2680 100644 --- a/servicetalk-http-netty/build.gradle +++ b/servicetalk-http-netty/build.gradle @@ -50,6 +50,7 @@ dependencies { testImplementation project(":servicetalk-encoding-netty") testImplementation project(":servicetalk-test-resources") testImplementation project(":servicetalk-utils-internal") + testImplementation project(":servicetalk-oio-api-internal") testImplementation "io.netty:netty-transport-native-unix-common:$nettyVersion" testImplementation "io.netty:netty-tcnative-boringssl-static:$tcnativeVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version" diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HeaderUtils.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HeaderUtils.java index 6c9c43abb3..371485298d 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HeaderUtils.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HeaderUtils.java @@ -362,8 +362,19 @@ private static IllegalArgumentException multipleCL(final CharSequence firstValue return new IllegalArgumentException("Multiple content-length values found: " + allClValues); } + /** + * A special consumer that takes an {@code int} and a custom argument and returns the result. + * + * @param The other argument to this function. + */ @FunctionalInterface private interface BiIntConsumer { - void apply(int contentLength, T headers); + /** + * Evaluates this consumer on the given arguments. + * + * @param i The {@code int} argument. + * @param t The {@link T} argument. + */ + void apply(int i, T t); } } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/NettyHttpServer.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/NettyHttpServer.java index 5ebbefeb48..feeb2b9205 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/NettyHttpServer.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/NettyHttpServer.java @@ -43,6 +43,7 @@ import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; import io.servicetalk.http.api.StreamingHttpService; +import io.servicetalk.serializer.api.SerializationException; import io.servicetalk.tcp.netty.internal.ReadOnlyTcpServerConfig; import io.servicetalk.tcp.netty.internal.TcpServerBinder; import io.servicetalk.tcp.netty.internal.TcpServerChannelInitializer; @@ -408,6 +409,9 @@ private StreamingHttpResponse newErrorResponse(final Throwable cause, final Exec LOGGER.error("Task rejected by Executor {} for service={}, connection={}", executor, service, this, cause); response = streamingResponseFactory().serviceUnavailable(); + } else if (cause instanceof SerializationException) { + // It is assumed that a failure occurred when attempting to deserialize the request. + response = streamingResponseFactory().unsupportedMediaType(); } else { LOGGER.error("Internal server error service={} connection={}", service, this, cause); response = streamingResponseFactory().internalServerError(); diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/StreamingHttpRequestWithContext.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/StreamingHttpRequestWithContext.java index 46a1f205aa..9c9de823c1 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/StreamingHttpRequestWithContext.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/StreamingHttpRequestWithContext.java @@ -18,6 +18,7 @@ import io.servicetalk.buffer.api.Buffer; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferEncoder; import io.servicetalk.encoding.api.ContentCodec; import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.HttpCookiePair; @@ -28,6 +29,8 @@ import io.servicetalk.http.api.HttpRequestMethod; import io.servicetalk.http.api.HttpSerializer; import io.servicetalk.http.api.HttpSetCookie; +import io.servicetalk.http.api.HttpStreamingDeserializer; +import io.servicetalk.http.api.HttpStreamingSerializer; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.TrailersTransformer; import io.servicetalk.http.netty.LoadBalancedStreamingHttpClient.OwnedRunnable; @@ -70,6 +73,7 @@ public HttpHeaders headers() { return delegate.headers(); } + @Deprecated @Nullable @Override public ContentCodec encoding() { @@ -198,16 +202,28 @@ public HostAndPort effectiveHostAndPort() { return delegate.effectiveHostAndPort(); } + @Nullable + @Override + public BufferEncoder contentEncoding() { + return delegate.contentEncoding(); + } + @Override public Publisher payloadBody() { return delegate.payloadBody(); } + @Deprecated @Override public Publisher payloadBody(final HttpDeserializer deserializer) { return delegate.payloadBody(deserializer); } + @Override + public Publisher payloadBody(final HttpStreamingDeserializer deserializer) { + return delegate.payloadBody(deserializer); + } + @Override public Publisher messageBody() { return delegate.messageBody(); @@ -219,12 +235,21 @@ public StreamingHttpRequest payloadBody(final Publisher payloadBody) { return this; } + @Deprecated @Override public StreamingHttpRequest payloadBody(final Publisher payloadBody, final HttpSerializer serializer) { delegate.payloadBody(payloadBody, serializer); return this; } + @Override + public StreamingHttpRequest payloadBody(final Publisher payloadBody, + final HttpStreamingSerializer serializer) { + delegate.payloadBody(payloadBody, serializer); + return this; + } + + @Deprecated @Override public StreamingHttpRequest transformPayloadBody(final Function, Publisher> transformer, final HttpSerializer serializer) { @@ -232,6 +257,14 @@ public StreamingHttpRequest transformPayloadBody(final Function StreamingHttpRequest transformPayloadBody(final Function, Publisher> transformer, + final HttpStreamingSerializer serializer) { + delegate.transformPayloadBody(transformer, serializer); + return this; + } + + @Deprecated @Override public StreamingHttpRequest transformPayloadBody(final Function, Publisher> transformer, final HttpDeserializer deserializer, @@ -240,6 +273,14 @@ public StreamingHttpRequest transformPayloadBody(final Function StreamingHttpRequest transformPayloadBody(final Function, Publisher> transformer, + final HttpStreamingDeserializer deserializer, + final HttpStreamingSerializer serializer) { + delegate.transformPayloadBody(transformer, deserializer, serializer); + return this; + } + @Override public StreamingHttpRequest transformPayloadBody(final UnaryOperator> transformer) { delegate.transformPayloadBody(transformer); @@ -258,6 +299,13 @@ public StreamingHttpRequest transform(final TrailersTransformer t return this; } + @Override + public StreamingHttpRequest transform(final TrailersTransformer trailersTransformer, + final HttpStreamingDeserializer deserializer) { + delegate.transform(trailersTransformer, deserializer); + return this; + } + @Override public Single toRequest() { return delegate.toRequest(); @@ -346,12 +394,19 @@ public StreamingHttpRequest method(final HttpRequestMethod method) { return this; } + @Deprecated @Override public StreamingHttpRequest encoding(final ContentCodec encoding) { delegate.encoding(encoding); return this; } + @Override + public StreamingHttpRequest contentEncoding(@Nullable final BufferEncoder encoder) { + delegate.contentEncoding(encoder); + return this; + } + @Override public StreamingHttpRequest requestTarget(final String requestTarget) { delegate.requestTarget(requestTarget); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractNettyHttpServerTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractNettyHttpServerTest.java index ca47fb4f19..0af04ee7f2 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractNettyHttpServerTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AbstractNettyHttpServerTest.java @@ -72,6 +72,7 @@ import static io.servicetalk.concurrent.api.BlockingTestUtils.awaitIndefinitelyNonNull; import static io.servicetalk.concurrent.api.Executors.newCachedThreadExecutor; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.logging.api.LogLevel.TRACE; import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname; @@ -296,6 +297,18 @@ void assertResponse(final StreamingHttpResponse response, final HttpProtocolVers assertThat(actualPayload, is(expectedPayload)); } + void assertSerializedResponse(final StreamingHttpResponse response, final HttpProtocolVersion version, + final HttpResponseStatus status, final String expectedPayload) + throws ExecutionException, InterruptedException { + assertResponse(response, version, status); + String actualPayload = response.payloadBody(appSerializerUtf8FixLen()) + .collect(StringBuilder::new, (sb, chunk) -> { + sb.append(chunk); + return sb; + }).toFuture().get().toString(); + assertThat(actualPayload, is(expectedPayload)); + } + static Publisher getChunkPublisherFromStrings(final String... texts) { return Publisher.from(texts).map(AbstractNettyHttpServerTest::getChunkFromString); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AlpnClientAndServerTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AlpnClientAndServerTest.java index 0566d4fdab..1415878d28 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AlpnClientAndServerTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/AlpnClientAndServerTest.java @@ -42,8 +42,7 @@ import static io.servicetalk.concurrent.api.VerificationTestUtils.assertThrows; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.AlpnIds.HTTP_1_1; import static io.servicetalk.http.netty.AlpnIds.HTTP_2; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; @@ -131,7 +130,7 @@ private ServerContext startServer(List supportedProtocols) throws Except .listenBlocking((ctx, request, responseFactory) -> { serviceContext.put(ctx); requestVersion.put(request.version()); - return responseFactory.ok().payloadBody(PAYLOAD_BODY, textSerializer()); + return responseFactory.ok().payloadBody(PAYLOAD_BODY, textSerializerUtf8()); }) .toFuture().get(); } @@ -208,7 +207,7 @@ void testAlpnClient(List serverSideProtocols, private void assertResponseAndServiceContext(HttpResponse response) throws Exception { assertThat(response.version(), is(expectedProtocol)); assertThat(response.status(), is(OK)); - assertThat(response.payloadBody(textDeserializer()), is(PAYLOAD_BODY)); + assertThat(response.payloadBody(textSerializerUtf8()), is(PAYLOAD_BODY)); HttpServiceContext serviceCtx = serviceContext.take(); assertThat(serviceCtx.protocol(), is(expectedProtocol)); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentCodingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentCodingTest.java index 8bdf2e1099..0476014087 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentCodingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentCodingTest.java @@ -35,6 +35,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +@Deprecated public abstract class BaseContentCodingTest { private static final int PAYLOAD_SIZE = 1024; diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentEncodingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentEncodingTest.java new file mode 100644 index 0000000000..03fb3a090b --- /dev/null +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BaseContentEncodingTest.java @@ -0,0 +1,202 @@ +/* + * Copyright © 2020-2021 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.http.netty; + +import io.servicetalk.encoding.api.BufferDecoderGroup; +import io.servicetalk.encoding.api.BufferDecoderGroupBuilder; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.encoding.api.EmptyBufferDecoderGroup; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.deflateDefault; +import static io.servicetalk.encoding.netty.NettyBufferEncoders.gzipDefault; +import static io.servicetalk.http.netty.HttpProtocol.HTTP_1; +import static io.servicetalk.http.netty.HttpProtocol.HTTP_2; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +abstract class BaseContentEncodingTest { + private static final int PAYLOAD_SIZE = 1024; + + private static Stream params() { + return Stream.of( + Arguments.of(HTTP_1, Encoder.DEFAULT, Decoders.DEFAULT, Encoders.DEFAULT, Decoders.DEFAULT, true), + Arguments.of(HTTP_2, Encoder.DEFAULT, Decoders.DEFAULT, Encoders.DEFAULT, Decoders.DEFAULT, true), + Arguments.of(HTTP_1, Encoder.GZIP, Decoders.GZIP_ID, Encoders.GZIP_ID, Decoders.GZIP_ID, true), + Arguments.of(HTTP_2, Encoder.GZIP, Decoders.GZIP_ID, Encoders.GZIP_ID, Decoders.GZIP_ID, true), + Arguments.of(HTTP_1, Encoder.DEFLATE, Decoders.DEFLATE_ID, Encoders.DEFLATE_ID, Decoders.DEFLATE_ID, + true), + Arguments.of(HTTP_2, Encoder.DEFLATE, Decoders.DEFLATE_ID, Encoders.DEFLATE_ID, Decoders.DEFLATE_ID, + true), + Arguments.of(HTTP_1, Encoder.GZIP, Decoders.GZIP_DEFLATE_ID, Encoders.GZIP_DEFLATE_ID, + Decoders.GZIP_DEFLATE_ID, true), + Arguments.of(HTTP_2, Encoder.GZIP, Decoders.GZIP_DEFLATE_ID, Encoders.GZIP_DEFLATE_ID, + Decoders.GZIP_DEFLATE_ID, true), + Arguments.of(HTTP_1, Encoder.DEFLATE, Decoders.GZIP_DEFLATE_ID, Encoders.GZIP_DEFLATE_ID, + Decoders.GZIP_DEFLATE_ID, true), + Arguments.of(HTTP_2, Encoder.DEFLATE, Decoders.GZIP_DEFLATE_ID, Encoders.GZIP_DEFLATE_ID, + Decoders.GZIP_DEFLATE_ID, true), + Arguments.of(HTTP_1, Encoder.GZIP, Decoders.ID_DEFLATE_GZIP, Encoders.ID_DEFLATE_GZIP, + Decoders.ID_DEFLATE_GZIP, true), + Arguments.of(HTTP_2, Encoder.GZIP, Decoders.ID_DEFLATE_GZIP, Encoders.ID_DEFLATE_GZIP, + Decoders.ID_DEFLATE_GZIP, true), + Arguments.of(HTTP_1, Encoder.DEFLATE, Decoders.ID_DEFLATE_GZIP, Encoders.ID_DEFLATE_GZIP, + Decoders.ID_DEFLATE_GZIP, true), + Arguments.of(HTTP_2, Encoder.DEFLATE, Decoders.ID_DEFLATE_GZIP, Encoders.ID_DEFLATE_GZIP, + Decoders.ID_DEFLATE_GZIP, true), + Arguments.of(HTTP_1, Encoder.ID, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.ID_ONLY, + true), + Arguments.of(HTTP_2, Encoder.ID, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.ID_ONLY, + true), + + // identity is currently always supported + Arguments.of(HTTP_1, Encoder.DEFAULT, Decoders.DEFAULT, Encoders.GZIP_ONLY, Decoders.DEFAULT, true), + Arguments.of(HTTP_2, Encoder.DEFAULT, Decoders.DEFAULT, Encoders.GZIP_ONLY, Decoders.DEFAULT, true), + Arguments.of(HTTP_1, Encoder.DEFAULT, Decoders.DEFAULT, Encoders.DEFLATE_ONLY, Decoders.DEFAULT, true), + Arguments.of(HTTP_2, Encoder.DEFAULT, Decoders.DEFAULT, Encoders.DEFLATE_ONLY, Decoders.DEFAULT, true), + + Arguments.of(HTTP_1, Encoder.GZIP, Decoders.DEFAULT, Encoders.DEFAULT, Decoders.DEFAULT, false), + Arguments.of(HTTP_2, Encoder.GZIP, Decoders.DEFAULT, Encoders.DEFAULT, Decoders.DEFAULT, false), + Arguments.of(HTTP_1, Encoder.DEFLATE, Decoders.DEFAULT, Encoders.DEFAULT, Decoders.DEFAULT, false), + Arguments.of(HTTP_2, Encoder.DEFLATE, Decoders.DEFAULT, Encoders.DEFAULT, Decoders.DEFAULT, false), + Arguments.of(HTTP_1, Encoder.GZIP, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.DEFAULT, false), + Arguments.of(HTTP_2, Encoder.GZIP, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.DEFAULT, false), + Arguments.of(HTTP_1, Encoder.DEFLATE, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.GZIP_ONLY, + false), + Arguments.of(HTTP_2, Encoder.DEFLATE, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.GZIP_ONLY, + false), + Arguments.of(HTTP_1, Encoder.ID, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.GZIP_ONLY, + true), + Arguments.of(HTTP_2, Encoder.ID, Decoders.GZIP_DEFLATE_ID, Encoders.DEFAULT, Decoders.GZIP_ONLY, + true)); + } + + @ParameterizedTest(name = "{index}, protocol={0}, client-encode=[{1}], client-decode=[{2}], server-encode={3}, " + + "server-decode={4}, valid={5}") + @MethodSource("params") + final void testCompatibility( + final HttpProtocol protocol, final Encoder clientEncoding, final Decoders clientDecoder, + final Encoders serverEncoder, final Decoders serverDecoder, final boolean valid) throws Throwable { + runTest(protocol, clientEncoding, clientDecoder, serverEncoder, serverDecoder, valid); + } + + protected abstract void runTest( + HttpProtocol protocol, Encoder clientEncoding, Decoders clientDecoder, + Encoders serverEncoder, Decoders serverDecoder, boolean isValid) throws Throwable; + + static byte[] payload(byte b) { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, b); + return payload; + } + + static String payloadAsString(byte b) { + return new String(payload(b), StandardCharsets.US_ASCII); + } + + protected enum Decoders { + DEFAULT(EmptyBufferDecoderGroup.INSTANCE), + GZIP_ONLY(new BufferDecoderGroupBuilder().add(gzipDefault(), true).build()), + GZIP_ID(new BufferDecoderGroupBuilder().add(gzipDefault(), true).add(identityEncoder(), false).build()), + GZIP_DEFLATE_ID(new BufferDecoderGroupBuilder().add(gzipDefault(), true).add(deflateDefault(), true) + .add(identityEncoder(), false).build()), + ID_ONLY(new BufferDecoderGroupBuilder().add(identityEncoder(), true).build()), + ID_GZIP(new BufferDecoderGroupBuilder().add(identityEncoder(), false).add(gzipDefault(), true).build()), + ID_DEFLATE(new BufferDecoderGroupBuilder().add(identityEncoder(), false).add(deflateDefault(), true).build()), + ID_DEFLATE_GZIP(new BufferDecoderGroupBuilder().add(identityEncoder(), false).add(deflateDefault(), true) + .add(gzipDefault(), true).build()), + DEFLATE_ONLY(new BufferDecoderGroupBuilder().add(deflateDefault(), true).build()), + DEFLATE_ID(new BufferDecoderGroupBuilder().add(deflateDefault(), true).add(identityEncoder(), false).build()); + + final BufferDecoderGroup group; + + Decoders(BufferDecoderGroup group) { + this.group = group; + } + + @Override + public String toString() { + return Objects.toString(group.advertisedMessageEncoding()); + } + } + + protected enum Encoders { + DEFAULT(emptyList()), + GZIP_ONLY(singletonList(gzipDefault())), + GZIP_ID(asList(gzipDefault(), identityEncoder())), + GZIP_DEFLATE_ID(asList(gzipDefault(), deflateDefault(), identityEncoder())), + ID_ONLY(singletonList(identityEncoder())), + ID_GZIP(asList(identityEncoder(), gzipDefault())), + ID_DEFLATE(asList(identityEncoder(), deflateDefault())), + ID_DEFLATE_GZIP(asList(identityEncoder(), deflateDefault(), gzipDefault())), + DEFLATE_ONLY(singletonList(deflateDefault())), + DEFLATE_ID(asList(deflateDefault(), identityEncoder())); + + final List list; + + Encoders(List list) { + this.list = list; + } + + @Override + public String toString() { + if (list.isEmpty()) { + return identityEncoder().encoder().toString(); + } + + StringBuilder b = new StringBuilder(); + for (BufferEncoder c : list) { + if (b.length() > 1) { + b.append(", "); + } + + b.append(c.encodingName()); + } + return b.toString(); + } + } + + protected enum Encoder { + DEFAULT(null), + ID(identityEncoder()), + GZIP(gzipDefault()), + DEFLATE(deflateDefault()); + + @Nullable + final BufferEncoder encoder; + + Encoder(@Nullable BufferEncoder encoder) { + this.encoder = encoder; + } + + @Override + public String toString() { + return encoder == null ? "null" : encoder.encodingName().toString(); + } + } +} diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BlockingStreamingHttpServiceTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BlockingStreamingHttpServiceTest.java index fc9f86fe30..c3357e8c40 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BlockingStreamingHttpServiceTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/BlockingStreamingHttpServiceTest.java @@ -16,11 +16,18 @@ package io.servicetalk.http.netty; import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; import io.servicetalk.concurrent.BlockingIterator; +import io.servicetalk.concurrent.internal.BlockingIterables; import io.servicetalk.http.api.BlockingStreamingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpRequest; import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.BlockingStreamingHttpService; +import io.servicetalk.http.api.DefaultHttpHeadersFactory; import io.servicetalk.http.api.HttpClient; +import io.servicetalk.http.api.HttpHeaders; +import io.servicetalk.http.api.HttpMessageBodyIterator; import io.servicetalk.http.api.HttpOutputStream; import io.servicetalk.http.api.HttpPayloadWriter; import io.servicetalk.http.api.HttpResponse; @@ -35,25 +42,33 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.annotation.Nullable; import static io.servicetalk.buffer.api.EmptyBuffer.EMPTY_BUFFER; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; import static io.servicetalk.http.api.HttpHeaderNames.TRAILER; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpResponseStatus.ACCEPTED; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static io.servicetalk.utils.internal.PlatformDependent.throwException; import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.StreamSupport.stream; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; @@ -123,12 +138,12 @@ void receivePayloadBody() throws Exception { StringBuilder receivedPayloadBody = new StringBuilder(); BlockingStreamingHttpClient client = context((ctx, request, response) -> { - request.payloadBody().forEach(chunk -> receivedPayloadBody.append(chunk.toString(US_ASCII))); + request.payloadBody(appSerializerUtf8FixLen()).forEach(receivedPayloadBody::append); response.sendMetaData().close(); }); BlockingStreamingHttpResponse response = client.request(client.post("/") - .payloadBody(asList("Hello\n", "World\n"), textSerializer())); + .payloadBody(asList("Hello\n", "World\n"), appSerializerUtf8FixLen())); assertResponse(response); assertThat(response.toResponse().toFuture().get().payloadBody(), is(EMPTY_BUFFER)); assertThat(receivedPayloadBody.toString(), is(HELLO_WORLD)); @@ -176,7 +191,7 @@ void respondWithPayloadBodyAndTrailersUsingPayloadWriter() throws Exception { try (HttpPayloadWriter pw = response.sendMetaData()) { pw.write(ctx.executionContext().bufferAllocator().fromAscii("Hello\n")); pw.write(ctx.executionContext().bufferAllocator().fromAscii("World\n")); - pw.setTrailer(X_TOTAL_LENGTH, String.valueOf("Hello\nWorld\n".length())); + pw.setTrailer(X_TOTAL_LENGTH, String.valueOf(HELLO_WORLD.length())); } }, false); } @@ -185,10 +200,10 @@ void respondWithPayloadBodyAndTrailersUsingPayloadWriter() throws Exception { void respondWithPayloadBodyAndTrailersUsingPayloadWriterWithSerializer() throws Exception { respondWithPayloadBodyAndTrailers((ctx, request, response) -> { response.setHeader(TRAILER, X_TOTAL_LENGTH); - try (HttpPayloadWriter pw = response.sendMetaData(textSerializer())) { + try (HttpPayloadWriter pw = response.sendMetaData(appSerializerUtf8FixLen())) { pw.write("Hello\n"); pw.write("World\n"); - pw.setTrailer(X_TOTAL_LENGTH, String.valueOf("Hello\nWorld\n".length())); + pw.setTrailer(X_TOTAL_LENGTH, String.valueOf(HELLO_WORLD.length())); } }, true); } @@ -200,11 +215,99 @@ void respondWithPayloadBodyAndTrailersUsingOutputStream() throws Exception { try (HttpOutputStream out = response.sendMetaDataOutputStream()) { out.write("Hello\n".getBytes(US_ASCII)); out.write("World\n".getBytes(US_ASCII)); - out.setTrailer(X_TOTAL_LENGTH, String.valueOf("Hello\nWorld\n".length())); + out.setTrailer(X_TOTAL_LENGTH, String.valueOf(HELLO_WORLD.length())); } }, false); } + @Test + void setRequestMessageBody() throws Exception { + BlockingStreamingHttpClient client = context((ctx, request, response) -> { + response.status(OK); + try { + HttpMessageBodyIterator reqItr = request.messageBody().iterator(); + StringBuilder sb = new StringBuilder(); + while (reqItr.hasNext()) { + sb.append(requireNonNull(reqItr.next()).toString(UTF_8)); + } + assertThat(sb.toString(), is(HELLO_WORLD)); + HttpHeaders trailers = reqItr.trailers(); + assertThat(trailers, notNullValue()); + assertThat(trailers.get(X_TOTAL_LENGTH).toString(), is(HELLO_WORLD_LENGTH)); + } catch (Throwable cause) { + HttpPayloadWriter payloadWriter = response.sendMetaData(appSerializerUtf8FixLen()); + payloadWriter.write(cause.toString()); + payloadWriter.close(); + return; + } + response.sendMetaData(appSerializerUtf8FixLen()).close(); + }); + BufferAllocator alloc = client.executionContext().bufferAllocator(); + BlockingStreamingHttpRequest req = client.get("/"); + req.setHeader(TRAILER, X_TOTAL_LENGTH); + int split = HELLO_WORLD.length() / 2; + final BlockingIterable reqIterable = + BlockingIterables.from(asList( + alloc.fromAscii(HELLO_WORLD.substring(0, split)), + alloc.fromAscii(HELLO_WORLD.substring(split)))); + req.messageBody(() -> new HttpMessageBodyIterator() { + private final BlockingIterator iterator = reqIterable.iterator(); + @Nullable + private HttpHeaders trailers; + private int totalLength; + + @Nullable + @Override + public HttpHeaders trailers() { + if (trailers == null) { + trailers = DefaultHttpHeadersFactory.INSTANCE.newTrailers(); + trailers.set(X_TOTAL_LENGTH, String.valueOf(totalLength)); + } + return trailers; + } + + @Override + public boolean hasNext(final long timeout, final TimeUnit unit) throws TimeoutException { + return iterator.hasNext(timeout, unit); + } + + @Nullable + @Override + public Buffer next(final long timeout, final TimeUnit unit) throws TimeoutException { + return addTotalLength(iterator.next(timeout, unit)); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Nullable + @Override + public Buffer next() { + return addTotalLength(iterator.next()); + } + + @Override + public void close() throws Exception { + iterator.close(); + } + + @Nullable + private Buffer addTotalLength(@Nullable Buffer buffer) { + if (buffer != null) { + totalLength += buffer.readableBytes(); + } + return buffer; + } + }); + + BlockingStreamingHttpResponse response = client.request(req); + assertThat(response.status(), is(OK)); + assertThat(stream(response.payloadBody(appSerializerUtf8FixLen()).spliterator(), false) + .collect(Collectors.toList()), emptyIterable()); + } + private void respondWithPayloadBodyAndTrailers(BlockingStreamingHttpService handler, boolean useDeserializer) throws Exception { BlockingStreamingHttpClient client = context(handler); @@ -213,18 +316,33 @@ private void respondWithPayloadBodyAndTrailers(BlockingStreamingHttpService hand assertResponse(response); assertThat(response.headers().get(TRAILER).toString(), is(X_TOTAL_LENGTH)); - HttpResponse aggregated = response.toResponse().toFuture().get(); + final StringBuilder sb = new StringBuilder(); + final HttpHeaders trailers; if (useDeserializer) { - assertThat(aggregated.payloadBody(textDeserializer()), is(HELLO_WORLD)); + HttpMessageBodyIterator msgBody = response.messageBody(appSerializerUtf8FixLen()).iterator(); + while (msgBody.hasNext()) { + sb.append(msgBody.next()); + } + trailers = msgBody.trailers(); } else { - assertThat(aggregated.payloadBody().toString(US_ASCII), is(HELLO_WORLD)); + HttpMessageBodyIterator msgBody = response.messageBody().iterator(); + while (msgBody.hasNext()) { + sb.append(requireNonNull(msgBody.next()).toString(UTF_8)); + } + trailers = msgBody.trailers(); } - assertThat(aggregated.trailers().get(X_TOTAL_LENGTH).toString(), is(HELLO_WORLD_LENGTH)); + assertThat(sb.toString(), is(HELLO_WORLD)); + assertThat(trailers, notNullValue()); + assertThat(trailers.get(X_TOTAL_LENGTH).toString(), is(HELLO_WORLD_LENGTH)); } @Test void echoServerUsingPayloadWriter() throws Exception { echoServer((ctx, request, response) -> { + CharSequence contentType = request.headers().get(CONTENT_TYPE); + if (contentType != null) { + response.setHeader(CONTENT_TYPE, contentType); + } try (PayloadWriter pw = response.sendMetaData()) { request.payloadBody().forEach(chunk -> { try { @@ -234,14 +352,14 @@ void echoServerUsingPayloadWriter() throws Exception { } }); } - }, false); + }); } @Test void echoServerUsingPayloadWriterWithSerializer() throws Exception { echoServer((ctx, request, response) -> { - try (PayloadWriter pw = response.sendMetaData(textSerializer())) { - request.payloadBody(textDeserializer()).forEach(chunk -> { + try (PayloadWriter pw = response.sendMetaData(appSerializerUtf8FixLen())) { + request.payloadBody(appSerializerUtf8FixLen()).forEach(chunk -> { try { pw.write(chunk); } catch (IOException e) { @@ -249,12 +367,16 @@ void echoServerUsingPayloadWriterWithSerializer() throws Exception { } }); } - }, true); + }); } @Test void echoServerUsingInputOutputStream() throws Exception { echoServer((ctx, request, response) -> { + CharSequence contentType = request.headers().get(CONTENT_TYPE); + if (contentType != null) { + response.setHeader(CONTENT_TYPE, contentType); + } try (OutputStream out = response.sendMetaDataOutputStream(); InputStream in = request.payloadBodyInputStream()) { int ch; @@ -262,15 +384,15 @@ void echoServerUsingInputOutputStream() throws Exception { out.write(ch); } } - }, false); + }); } - private void echoServer(BlockingStreamingHttpService handler, boolean useDeserializer) throws Exception { + private void echoServer(BlockingStreamingHttpService handler) throws Exception { BlockingStreamingHttpClient client = context(handler); BlockingStreamingHttpResponse response = client.request(client.post("/") - .payloadBody(asList("Hello\n", "World\n"), textSerializer())); - assertResponse(response, HELLO_WORLD, useDeserializer); + .payloadBody(asList("Hello\n", "World\n"), appSerializerUtf8FixLen())); + assertResponse(response, HELLO_WORLD, true); } @Test @@ -332,12 +454,14 @@ private static void assertResponse(BlockingStreamingHttpResponse response, String expectedPayloadBody, boolean useDeserializer) throws Exception { assertResponse(response); - - HttpResponse aggregated = response.toResponse().toFuture().get(); if (useDeserializer) { - assertThat(aggregated.payloadBody(textDeserializer()), is(expectedPayloadBody)); + StringBuilder sb = new StringBuilder(); + for (String s : response.payloadBody(appSerializerUtf8FixLen())) { + sb.append(s); + } + assertThat(sb.toString(), is(expectedPayloadBody)); } else { - assertThat(aggregated.payloadBody().toString(US_ASCII), is(expectedPayloadBody)); + assertThat(response.toResponse().toFuture().get().payloadBody().toString(UTF_8), is(expectedPayloadBody)); } } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientClosureRaceTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientClosureRaceTest.java index 00c2017a20..14282bba23 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientClosureRaceTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ClientClosureRaceTest.java @@ -44,7 +44,7 @@ import javax.annotation.Nullable; import static io.servicetalk.concurrent.api.Single.collectUnordered; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1; import static java.net.InetAddress.getLoopbackAddress; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -130,8 +130,9 @@ void testSequential() throws Exception { void testSequentialPosts() throws Exception { try (HttpClient client = newClientBuilder().build()) { runIterations(() -> - client.request(client.post("/foo").payloadBody("Some payload", textSerializer())).flatMap( - response -> client.request(client.post("/bar").payloadBody("Another payload", textSerializer())))); + client.request(client.post("/foo").payloadBody("Some payload", textSerializerUtf8())) + .flatMap(response -> client.request(client.post("/bar") + .payloadBody("Another payload", textSerializerUtf8())))); } } @@ -151,8 +152,8 @@ void testPipelinedPosts() throws Exception { .protocols(h1().maxPipelinedRequests(2).build()) .build()) { runIterations(() -> collectUnordered( - client.request(client.get("/foo").payloadBody("Some payload", textSerializer())), - client.request(client.get("/bar").payloadBody("Another payload", textSerializer())))); + client.request(client.get("/foo").payloadBody("Some payload", textSerializerUtf8())), + client.request(client.get("/bar").payloadBody("Another payload", textSerializerUtf8())))); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionCloseHeaderHandlingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionCloseHeaderHandlingTest.java index 6e72576bee..1615c9dacb 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionCloseHeaderHandlingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionCloseHeaderHandlingTest.java @@ -24,6 +24,7 @@ import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; +import io.servicetalk.oio.api.internal.PayloadWriterUtils; import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; import io.servicetalk.transport.api.ConnectionContext; @@ -39,6 +40,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.channels.ClosedChannelException; import java.util.ArrayList; @@ -66,7 +69,8 @@ import static io.servicetalk.http.api.HttpRequestMethod.GET; import static io.servicetalk.http.api.HttpRequestMethod.POST; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.netty.ContentLengthAndTrailersTest.addFixedLengthFramingOverhead; import static io.servicetalk.http.netty.HttpsProxyTest.safeClose; import static io.servicetalk.logging.api.LogLevel.TRACE; import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname; @@ -86,6 +90,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; final class ConnectionCloseHeaderHandlingTest { + private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionCloseHeaderHandlingTest.class); private static final Collection TRUE_FALSE = asList(true, false); private static final String SERVER_SHOULD_CLOSE = "serverShouldClose"; @@ -154,7 +159,8 @@ public Completable accept(final ConnectionContext context) { requestReceived.countDown(); boolean noResponseContent = request.hasQueryParameter("noResponseContent", "true"); String content = noResponseContent ? "" : "server_content"; - response.addHeader(CONTENT_LENGTH, noResponseContent ? ZERO : valueOf(content.length())); + response.addHeader(CONTENT_LENGTH, noResponseContent ? ZERO : + valueOf(addFixedLengthFramingOverhead(content.length()))); // Add the "connection: close" header only when requested: if (request.hasQueryParameter(SERVER_SHOULD_CLOSE)) { @@ -162,20 +168,24 @@ public Completable accept(final ConnectionContext context) { } sendResponse.await(); - try (HttpPayloadWriter writer = response.sendMetaData(textSerializer())) { + try (HttpPayloadWriter writer = response.sendMetaData(appSerializerUtf8FixLen())) { // Subscribe to the request payload body before response writer closes BlockingIterator iterator = request.payloadBody().iterator(); // Consume request payload body asynchronously: - ctx.executionContext().executor().execute(() -> { + Future writeFuture = ctx.executionContext().executor().submit(() -> { while (iterator.hasNext()) { Buffer chunk = iterator.next(); assert chunk != null; requestPayloadSize.addAndGet(chunk.readableBytes()); } - requestPayloadReceived.countDown(); - }); + }).beforeOnError(cause -> { + LOGGER.error("failure while writing response", cause); + PayloadWriterUtils.safeClose(writer, cause); + }) + .afterFinally(requestPayloadReceived::countDown) + .toFuture(); if (awaitRequestPayload) { - requestPayloadReceived.await(); + writeFuture.get(); } if (!noResponseContent) { // Defer payload body to see how client-side processes "Connection: close" header @@ -295,7 +305,7 @@ void testConnectionClosure(boolean useUds, boolean viaProxy, boolean awaitReques } catch (InterruptedException e) { throwException(e); } - }).concat(from(content)), textSerializer()); + }).concat(from(content)), appSerializerUtf8FixLen()); } if (requestInitiatesClosure) { request.addHeader(CONNECTION, CLOSE); @@ -382,7 +392,7 @@ void serverCloseSecondPipelinedRequestWriteAborted(boolean useUds, boolean viaPr String content = "request_content"; connection.request(connection.get("/second") .addHeader(CONTENT_LENGTH, valueOf(content.length())) - .payloadBody(from(content).concat(never()), textSerializer())) + .payloadBody(from(content).concat(never()), appSerializerUtf8FixLen())) .whenOnError(secondRequestError::set) .whenFinally(secondResponseReceived::countDown) .subscribe(second -> { }); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionContextToStringTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionContextToStringTest.java index 692c061425..60c1e4a7c9 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionContextToStringTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConnectionContextToStringTest.java @@ -24,8 +24,7 @@ import static io.servicetalk.http.api.HttpApiConversions.toStreamingHttpService; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED_SERVER; import static org.hamcrest.MatcherAssert.assertThat; @@ -44,7 +43,7 @@ private void setUp(HttpProtocol protocol) { @Override void service(final StreamingHttpService service) { super.service((toStreamingHttpService((BlockingHttpService) (ctx, request, responseFactory) -> - responseFactory.ok().payloadBody(ctx.toString(), textSerializer()), + responseFactory.ok().payloadBody(ctx.toString(), textSerializerUtf8()), strategy -> strategy)).adaptor()); } @@ -54,7 +53,7 @@ void test(HttpProtocol httpProtocol) throws Exception { setUp(httpProtocol); StreamingHttpResponse response = makeRequest(streamingHttpConnection().get("/")); assertResponse(response, protocol.version, OK); - String serverContext = response.toResponse().toFuture().get().payloadBody(textDeserializer()); + String serverContext = response.toResponse().toFuture().get().payloadBody(textSerializerUtf8()); assertThat("Client's ConnectionContext does not contain netty channel id", streamingHttpConnection().connectionContext().toString(), containsString("[id: ")); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConsumeRequestPayloadOnResponsePathTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConsumeRequestPayloadOnResponsePathTest.java index 7e9e0ebfc3..84461226e9 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConsumeRequestPayloadOnResponsePathTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ConsumeRequestPayloadOnResponsePathTest.java @@ -44,7 +44,8 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.api.HttpHeaderNames.TRAILER; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.api.StreamingHttpResponses.newTransportResponse; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static java.nio.charset.StandardCharsets.UTF_8; @@ -152,7 +153,7 @@ public Single handle(final HttpServiceContext ctx, .listenStreamingAndAwait((ctx, request, responseFactory) -> { final StreamingHttpResponse response = responseFactory.ok() .addHeader(TRAILER, X_TOTAL_LENGTH) - .payloadBody(from("Response\n", "Payload\n", "Body\n"), textSerializer()) + .payloadBody(from("Response\n", "Payload\n", "Body\n"), appSerializerUtf8FixLen()) .transform(new TrailersTransformer() { @Override public AtomicInteger newState() { @@ -185,7 +186,7 @@ public HttpHeaders catchPayloadFailure(final AtomicInteger __, final Throwable _ HttpResponse response; try (BlockingHttpClient client = HttpClients.forSingleAddress(AddressUtils.serverHostAndPort(serverContext)) .buildBlocking()) { - response = client.request(client.post("/").payloadBody(EXPECTED_REQUEST_PAYLOAD, textSerializer())); + response = client.request(client.post("/").payloadBody(EXPECTED_REQUEST_PAYLOAD, textSerializerUtf8())); serverLatch.await(); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentHeadersTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentHeadersTest.java index d2bb5c7884..94c3d38ad3 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentHeadersTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentHeadersTest.java @@ -68,7 +68,8 @@ import static io.servicetalk.http.api.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.servicetalk.http.api.HttpResponseStatus.NO_CONTENT; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.api.StreamingHttpRequests.newRequest; import static io.servicetalk.http.api.StreamingHttpResponses.newResponse; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED; @@ -261,9 +262,9 @@ private static UnaryOperator contentLength() { return describe(input -> { input.headers().set(CONTENT_LENGTH, Integer.toString(EXISTING_CONTENT_LENGTH)); if (input instanceof HttpRequest) { - return ((HttpRequest) input).payloadBody(EXISTING_CONTENT, textSerializer()); + return ((HttpRequest) input).payloadBody(EXISTING_CONTENT, textSerializerUtf8()); } else if (input instanceof HttpResponse) { - return ((HttpResponse) input).payloadBody(EXISTING_CONTENT, textSerializer()); + return ((HttpResponse) input).payloadBody(EXISTING_CONTENT, textSerializerUtf8()); } else { fail("Unexpected metadata type: " + input.getClass()); throw new IllegalStateException(); @@ -312,23 +313,23 @@ Single listen(final HttpServerBuilder builder) { private static HttpRequest newAggregatedRequest(final HttpRequestMethod requestMethod) { return awaitSingleIndefinitelyNonNull(newRequest(requestMethod, "/", HTTP_1_1, headersFactory.newHeaders(), DEFAULT_ALLOCATOR, headersFactory).toRequest()) - .payloadBody(PAYLOAD, textSerializer()); + .payloadBody(PAYLOAD, textSerializerUtf8()); } private static StreamingHttpRequest newStreamingRequest(final HttpRequestMethod requestMethod) { return newRequest(requestMethod, "/", HTTP_1_1, headersFactory.newHeaders(), - DEFAULT_ALLOCATOR, headersFactory).payloadBody(from(PAYLOAD), textSerializer()); + DEFAULT_ALLOCATOR, headersFactory).payloadBody(from(PAYLOAD), appSerializerUtf8FixLen()); } private static HttpResponse newAggregatedResponse(final HttpResponseStatus status) { return awaitSingleIndefinitelyNonNull(newResponse(status, HTTP_1_1, headersFactory.newHeaders(), - DEFAULT_ALLOCATOR, headersFactory).toResponse()).payloadBody(PAYLOAD, textSerializer()); + DEFAULT_ALLOCATOR, headersFactory).toResponse()).payloadBody(PAYLOAD, textSerializerUtf8()); } private static StreamingHttpResponse newStreamingResponse(final HttpResponseStatus status) { return newResponse(status, HTTP_1_1, headersFactory.newHeaders(), DEFAULT_ALLOCATOR, headersFactory) - .payloadBody(from(PAYLOAD), textSerializer()); + .payloadBody(from(PAYLOAD), appSerializerUtf8FixLen()); } private static UnaryOperator describe(UnaryOperator operator, String description) { @@ -435,7 +436,7 @@ Single listen(final HttpServerBuilder builder) { if (failure != null) { return succeeded(rf.internalServerError().payloadBody( from(failure), - textSerializer())); + appSerializerUtf8FixLen())); } return succeeded(rf.noContent()); }); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthAndTrailersTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthAndTrailersTest.java index 972ecbf459..594fe7cb48 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthAndTrailersTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthAndTrailersTest.java @@ -19,7 +19,6 @@ import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.HttpHeaders; import io.servicetalk.http.api.HttpMetaData; -import io.servicetalk.http.api.HttpResponse; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StatelessTrailersTransformer; import io.servicetalk.http.api.StreamingHttpRequest; @@ -27,6 +26,7 @@ import io.servicetalk.http.api.StreamingHttpResponseFactory; import io.servicetalk.http.api.StreamingHttpService; import io.servicetalk.http.api.StreamingHttpServiceFilter; +import io.servicetalk.http.api.TrailersTransformer; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; import static io.servicetalk.buffer.api.CharSequences.newAsciiString; import static io.servicetalk.buffer.api.Matchers.contentEqualTo; @@ -43,13 +44,12 @@ import static io.servicetalk.http.api.HttpHeaderNames.TRANSFER_ENCODING; import static io.servicetalk.http.api.HttpHeaderValues.CHUNKED; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED_SERVER; import static io.servicetalk.http.netty.HttpProtocol.HTTP_1; import static io.servicetalk.http.netty.HttpProtocol.HTTP_2; import static java.lang.String.valueOf; -import static java.nio.charset.StandardCharsets.US_ASCII; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; @@ -147,7 +147,8 @@ void contentLengthAddedAutomaticallyByAggregatedApiConversion(HttpProtocol proto @MethodSource("data") void contentLengthAddedManually(HttpProtocol protocol, String content) throws Exception { setUp(protocol, content); - test(r -> r.setHeader(CONTENT_LENGTH, valueOf(content.length())), r -> r, true, false, false); + test(r -> r.setHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(content.length()))), r -> r, true, + false, false); } @ParameterizedTest(name = "protocol={0}") @@ -201,7 +202,7 @@ void trailersAndContentLengthAddedForAggregatedRequest(HttpProtocol protocol, String content) throws Exception { setUp(protocol, content); test(r -> r.toRequest().toFuture().get() - .setHeader(CONTENT_LENGTH, valueOf(content.length())) + .setHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(content.length()))) .addTrailer(TRAILER_NAME, TRAILER_VALUE) .toStreamingRequest(), // HTTP/2 may have content-length and trailers at the same time @@ -213,7 +214,7 @@ void trailersAndContentLengthAddedForAggregatedRequest(HttpProtocol protocol, void trailersAndContentLengthAddedForStreamingRequest(HttpProtocol protocol, String content) throws Exception { setUp(protocol, content); - test(r -> r.setHeader(CONTENT_LENGTH, valueOf(content.length())) + test(r -> r.setHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(content.length()))) .transform(new StatelessTrailersTransformer() { @Override @@ -259,7 +260,7 @@ void trailersContentLengthAndTransferEncodingAddedForAggregatedRequest(HttpProto String content) throws Exception { setUp(protocol, content); test(r -> r.toRequest().toFuture().get() - .setHeader(CONTENT_LENGTH, valueOf(content.length())) + .setHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(content.length()))) .setHeader(TRANSFER_ENCODING, CHUNKED) .addTrailer(TRAILER_NAME, TRAILER_VALUE) .toStreamingRequest(), @@ -272,7 +273,7 @@ void trailersContentLengthAndTransferEncodingAddedForAggregatedRequest(HttpProto void trailersContentLengthAndTransferEncodingAddedForStreamingRequest(HttpProtocol protocol, String content) throws Exception { setUp(protocol, content); - test(r -> r.setHeader(CONTENT_LENGTH, valueOf(content.length())) + test(r -> r.setHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(content.length()))) .setHeader(TRANSFER_ENCODING, CHUNKED) .transform(new StatelessTrailersTransformer() { @@ -318,25 +319,78 @@ private void test(Transformer requestTransformer, StreamingHttpRequest preRequest = streamingHttpConnection().post("/"); if (!content.isEmpty()) { - preRequest.payloadBody(from(content), textSerializer()); + preRequest.payloadBody(from(content), appSerializerUtf8FixLen()); } StreamingHttpRequest request = requestTransformer.transform(preRequest); - HttpResponse response = responseTransformer.transform(makeRequest(request)).toResponse().toFuture().get(); + StreamingHttpResponse response = responseTransformer.transform(makeRequest(request)); assertResponse(response, protocol.version, OK); - assertThat(response.payloadBody().toString(US_ASCII), equalTo(content)); - HttpHeaders headers = response.headers(); assertThat("Unexpected content-length on the response", mergeValues(headers.values(CONTENT_LENGTH)), - contentEqualTo(hasContentLength ? valueOf(content.length()) : "")); + contentEqualTo(hasContentLength ? valueOf(addFixedLengthFramingOverhead(content.length())) : "")); assertThat("Unexpected transfer-encoding on the response", mergeValues(headers.values(TRANSFER_ENCODING)), contentEqualTo(chunked ? CHUNKED : "")); assertThat("Unexpected content-length on the request", headers.get(CLIENT_CONTENT_LENGTH), - hasContentLength ? contentEqualTo(valueOf(content.length())) : nullValue()); + hasContentLength ? contentEqualTo(valueOf(addFixedLengthFramingOverhead(content.length()))) : + nullValue()); assertThat("Unexpected transfer-encoding on the request", headers.get(CLIENT_TRANSFER_ENCODING), chunked ? contentEqualTo(CHUNKED) : nullValue()); - assertThat("Unexpected trailers on the request", response.trailers().get(TRAILER_NAME), - hasTrailers ? contentEqualTo(TRAILER_VALUE) : nullValue()); + + if (content.isEmpty()) { + response.transform(new TrailersTransformer() { + @Nullable + @Override + public Integer newState() { + return null; + } + + @Override + public Buffer accept(@Nullable final Object o, final Buffer buffer) { + assertThat(buffer.readableBytes(), equalTo(0)); + return buffer; + } + + @Override + public HttpHeaders payloadComplete(@Nullable final Object o, final HttpHeaders trailers) { + assertThat("Unexpected trailers on the request", trailers.get(TRAILER_NAME), + hasTrailers ? contentEqualTo(TRAILER_VALUE) : nullValue()); + return trailers; + } + + @Override + public HttpHeaders catchPayloadFailure(@Nullable final Object o, final Throwable cause, + final HttpHeaders trailers) throws Throwable { + throw cause; + } + }).messageBody().ignoreElements().toFuture().get(); + } else { + response.transform(new TrailersTransformer() { + @Override + public StringBuilder newState() { + return new StringBuilder(); + } + + @Override + public String accept(final StringBuilder o, final String s) { + o.append(s); + return s; + } + + @Override + public HttpHeaders payloadComplete(final StringBuilder o, final HttpHeaders trailers) { + assertThat(o.toString(), equalTo(content)); + assertThat("Unexpected trailers on the request", trailers.get(TRAILER_NAME), + hasTrailers ? contentEqualTo(TRAILER_VALUE) : nullValue()); + return trailers; + } + + @Override + public HttpHeaders catchPayloadFailure(@Nullable final StringBuilder o, final Throwable cause, + final HttpHeaders trailers) throws Throwable { + throw cause; + } + }, appSerializerUtf8FixLen()).messageBody().ignoreElements().toFuture().get(); + } } private static CharSequence mergeValues(Iterable values) { @@ -350,6 +404,10 @@ private static CharSequence mergeValues(Iterable values) return sb; } + static int addFixedLengthFramingOverhead(int length) { + return length == 0 ? 0 : length + Integer.BYTES; + } + private interface Transformer { T transform(T request) throws Exception; } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthTest.java index 138e407a52..513e901b16 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ContentLengthTest.java @@ -36,6 +36,7 @@ import static io.servicetalk.buffer.api.Matchers.contentEqualTo; import static io.servicetalk.buffer.api.ReadOnlyBufferAllocators.DEFAULT_RO_ALLOCATOR; import static io.servicetalk.buffer.netty.BufferAllocators.DEFAULT_ALLOCATOR; +import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LENGTH; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpRequestMethod.CONNECT; @@ -48,8 +49,8 @@ import static io.servicetalk.http.api.HttpRequestMethod.PUT; import static io.servicetalk.http.api.HttpRequestMethod.TRACE; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.api.StreamingHttpRequests.newRequest; import static io.servicetalk.http.api.StreamingHttpResponses.newResponse; import static io.servicetalk.http.netty.HeaderUtils.setRequestContentLength; @@ -110,37 +111,37 @@ private static void shouldCalculateRequestContentLengthFromEmptyPublisher(HttpRe @Test void shouldCalculateRequestContentLengthFromSingleItemPublisher() throws Exception { StreamingHttpRequest request = newAggregatedRequest().toStreamingRequest() - .payloadBody(Publisher.from("Hello"), textSerializer()); - setRequestContentLengthAndVerify(request, contentEqualTo("5")); + .payloadBody(from("Hello"), appSerializerUtf8FixLen()); + setRequestContentLengthAndVerify(request, contentEqualTo("9")); } @Test void shouldCalculateRequestContentLengthFromTwoItemPublisher() throws Exception { StreamingHttpRequest request = newAggregatedRequest().toStreamingRequest() - .payloadBody(Publisher.from("Hello", "World"), textSerializer()); - setRequestContentLengthAndVerify(request, contentEqualTo("10")); + .payloadBody(from("Hello", "World"), appSerializerUtf8FixLen()); + setRequestContentLengthAndVerify(request, contentEqualTo("18")); } @Test void shouldCalculateRequestContentLengthFromMultipleItemPublisher() throws Exception { StreamingHttpRequest request = newAggregatedRequest().toStreamingRequest() - .payloadBody(Publisher.from("Hello", " ", "World", "!"), textSerializer()); - setRequestContentLengthAndVerify(request, contentEqualTo("12")); + .payloadBody(from("Hello", " ", "World", "!"), appSerializerUtf8FixLen()); + setRequestContentLengthAndVerify(request, contentEqualTo("28")); } @Test void shouldCalculateRequestContentLengthFromTransformedMultipleItemPublisher() throws Exception { - StreamingHttpRequest request = newAggregatedRequest().payloadBody("Hello", textSerializer()) - .toStreamingRequest().transformPayloadBody(payload -> payload.concat(Publisher.from(" ", "World", "!")), - textDeserializer(), textSerializer()); - setRequestContentLengthAndVerify(request, contentEqualTo("12")); + StreamingHttpRequest request = newStreamingRequest().payloadBody(from("Hello"), appSerializerUtf8FixLen()) + .transformPayloadBody(payload -> payload.concat(from(" ", "World", "!")), + appSerializerUtf8FixLen(), appSerializerUtf8FixLen()); + setRequestContentLengthAndVerify(request, contentEqualTo("28")); } @Test void shouldCalculateRequestContentLengthFromTransformedRawMultipleItemPublisher() throws Exception { - StreamingHttpRequest request = newAggregatedRequest().payloadBody("Hello", textSerializer()) + StreamingHttpRequest request = newAggregatedRequest().payloadBody("Hello", textSerializerUtf8()) .toStreamingRequest().transformMessageBody(payload -> payload.map(obj -> (Buffer) obj) - .concat(Publisher.from(" ", "World", "!").map(DEFAULT_RO_ALLOCATOR::fromAscii))); + .concat(from(" ", "World", "!").map(DEFAULT_RO_ALLOCATOR::fromAscii))); setRequestContentLengthAndVerify(request, contentEqualTo("12")); } @@ -160,31 +161,30 @@ void shouldCalculateResponseContentLengthFromEmptyPublisher() throws Exception { @Test void shouldCalculateResponseContentLengthFromSingleItemPublisher() throws Exception { StreamingHttpResponse response = newAggregatedResponse().toStreamingResponse() - .payloadBody(Publisher.from("Hello"), textSerializer()); - setResponseContentLengthAndVerify(response, contentEqualTo("5")); + .payloadBody(from("Hello"), appSerializerUtf8FixLen()); + setResponseContentLengthAndVerify(response, contentEqualTo("9")); } @Test void shouldCalculateResponseContentLengthFromTwoItemPublisher() throws Exception { StreamingHttpResponse response = newAggregatedResponse().toStreamingResponse() - .payloadBody(Publisher.from("Hello", "World"), textSerializer()); - setResponseContentLengthAndVerify(response, contentEqualTo("10")); + .payloadBody(from("Hello", "World"), appSerializerUtf8FixLen()); + setResponseContentLengthAndVerify(response, contentEqualTo("18")); } @Test void shouldCalculateResponseContentLengthFromMultipleItemPublisher() throws Exception { StreamingHttpResponse response = newAggregatedResponse().toStreamingResponse() - .payloadBody(Publisher.from("Hello", " ", "World", "!"), textSerializer()); - setResponseContentLengthAndVerify(response, contentEqualTo("12")); + .payloadBody(from("Hello", " ", "World", "!"), appSerializerUtf8FixLen()); + setResponseContentLengthAndVerify(response, contentEqualTo("28")); } @Test void shouldCalculateResponseContentLengthFromTransformedMultipleItemPublisher() throws Exception { - StreamingHttpResponse response = newAggregatedResponse().payloadBody("Hello", textSerializer()) - .toStreamingResponse() - .transformPayloadBody(payload -> payload.concat(Publisher.from(" ", "World", "!")), - textDeserializer(), textSerializer()); - setResponseContentLengthAndVerify(response, contentEqualTo("12")); + StreamingHttpResponse response = newStreamingResponse().payloadBody(from("Hello"), appSerializerUtf8FixLen()) + .transformPayloadBody(payload -> payload.concat(from(" ", "World", "!")), + appSerializerUtf8FixLen(), appSerializerUtf8FixLen()); + setResponseContentLengthAndVerify(response, contentEqualTo("28")); } private static HttpRequest newAggregatedRequest() throws Exception { @@ -192,13 +192,23 @@ private static HttpRequest newAggregatedRequest() throws Exception { } private static HttpRequest newAggregatedRequest(HttpRequestMethod method) throws Exception { - return newRequest(method, "/", HTTP_1_1, headersFactory.newHeaders(), DEFAULT_ALLOCATOR, headersFactory) - .toRequest().toFuture().get(); + return newStreamingRequest(method).toRequest().toFuture().get(); + } + + private static StreamingHttpRequest newStreamingRequest() { + return newStreamingRequest(GET); + } + + private static StreamingHttpRequest newStreamingRequest(HttpRequestMethod method) { + return newRequest(method, "/", HTTP_1_1, headersFactory.newHeaders(), DEFAULT_ALLOCATOR, headersFactory); } private static HttpResponse newAggregatedResponse() throws Exception { - return newResponse(OK, HTTP_1_1, headersFactory.newHeaders(), DEFAULT_ALLOCATOR, headersFactory) - .toResponse().toFuture().get(); + return newStreamingResponse().toResponse().toFuture().get(); + } + + private static StreamingHttpResponse newStreamingResponse() { + return newResponse(OK, HTTP_1_1, headersFactory.newHeaders(), DEFAULT_ALLOCATOR, headersFactory); } private static void setRequestContentLengthAndVerify(final StreamingHttpRequest request, diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/FlushStrategyOnServerTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/FlushStrategyOnServerTest.java index 14068e7b0a..e3995b70f1 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/FlushStrategyOnServerTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/FlushStrategyOnServerTest.java @@ -55,7 +55,7 @@ import static io.servicetalk.http.api.HttpHeaderValues.CHUNKED; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpRequestMethod.GET; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.api.StreamingHttpRequests.newTransportRequest; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.http.netty.NettyHttpServer.initChannel; @@ -97,7 +97,7 @@ private void setUp(final Param param) { final StreamingHttpService service = (ctx, request, responseFactory) -> { StreamingHttpResponse resp = responseFactory.ok(); if (request.headers().get(USE_EMPTY_RESP_BODY) == null) { - resp.payloadBody(from("Hello", "World"), textSerializer()); + resp.payloadBody(from("Hello", "World"), appSerializerUtf8FixLen()); } if (request.headers().get(USE_AGGREGATED_RESP) != null) { return resp.toResponse().map(HttpResponse::toStreamingResponse); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulCloseTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulCloseTest.java index ed1e42e3c0..ca0a819538 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulCloseTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulCloseTest.java @@ -32,7 +32,7 @@ import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.api.DefaultHttpHeadersFactory.INSTANCE; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static org.hamcrest.MatcherAssert.assertThat; @@ -52,7 +52,7 @@ private enum TrailerAddType { @SuppressWarnings("unchecked") private void setUp(final TrailerAddType trailerAddType) throws Exception { context = HttpServers.forAddress(localAddress(0)).listenStreamingAndAwait((ctx, request, responseFactory) -> { - StreamingHttpResponse resp = responseFactory.ok().payloadBody(from("Hello"), textSerializer()); + StreamingHttpResponse resp = responseFactory.ok().payloadBody(from("Hello"), appSerializerUtf8FixLen()); switch (trailerAddType) { case Regular: resp.transform(new StaticTrailersTransformer()); @@ -81,7 +81,7 @@ void useConnection(TrailerAddType trailerAddType) throws Exception { setUp(trailerAddType); ReservedStreamingHttpConnection conn = client.reserveConnection(client.get("/")).toFuture().get(); StreamingHttpResponse resp = conn.request(client.get("/") - .payloadBody(from("Hello"), textSerializer())).toFuture().get(); + .payloadBody(from("Hello"), appSerializerUtf8FixLen())).toFuture().get(); assertThat("Unexpected response.", resp.status().code(), equalTo(HttpResponseStatus.OK.code())); // Drain response. resp.payloadBody().toFuture().get(); @@ -93,7 +93,7 @@ void useConnection(TrailerAddType trailerAddType) throws Exception { void useClient(TrailerAddType trailerAddType) throws Exception { setUp(trailerAddType); StreamingHttpResponse resp = client.request(client.get("/") - .payloadBody(from("Hello"), textSerializer())).toFuture().get(); + .payloadBody(from("Hello"), appSerializerUtf8FixLen())).toFuture().get(); assertThat("Unexpected response.", resp.status().code(), equalTo(HttpResponseStatus.OK.code())); // Drain response. resp.payloadBody().toFuture().get(); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulConnectionClosureHandlingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulConnectionClosureHandlingTest.java index 80937cf3f7..02a500b93f 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulConnectionClosureHandlingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/GracefulConnectionClosureHandlingTest.java @@ -29,6 +29,7 @@ import io.servicetalk.http.api.StreamingHttpClient; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; +import io.servicetalk.oio.api.internal.PayloadWriterUtils; import io.servicetalk.test.resources.DefaultTestCerts; import io.servicetalk.transport.api.ClientSslConfigBuilder; import io.servicetalk.transport.api.ConnectionContext; @@ -49,6 +50,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.SocketAddress; @@ -71,7 +74,8 @@ import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LENGTH; import static io.servicetalk.http.api.HttpHeaderValues.ZERO; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.netty.ContentLengthAndTrailersTest.addFixedLengthFramingOverhead; import static io.servicetalk.http.netty.HttpClients.forResolvedAddress; import static io.servicetalk.http.netty.HttpClients.forSingleAddressViaProxy; import static io.servicetalk.http.netty.HttpProtocol.HTTP_2; @@ -98,7 +102,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class GracefulConnectionClosureHandlingTest { - + private static final Logger LOGGER = LoggerFactory.getLogger(GracefulConnectionClosureHandlingTest.class); private static final Collection TRUE_FALSE = asList(true, false); @RegisterExtension @@ -181,14 +185,14 @@ public Completable accept(final ConnectionContext context) { serverContext = serverBuilder.listenBlockingStreamingAndAwait((ctx, request, response) -> { serverReceivedRequest.countDown(); - response.addHeader(CONTENT_LENGTH, valueOf(RESPONSE_CONTENT.length())); + response.addHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(RESPONSE_CONTENT.length()))); serverSendResponse.await(); - try (HttpPayloadWriter writer = response.sendMetaData(textSerializer())) { + try (HttpPayloadWriter writer = response.sendMetaData(appSerializerUtf8FixLen())) { // Subscribe to the request payload body before response writer closes BlockingIterator iterator = request.payloadBody().iterator(); // Consume request payload body asynchronously: - ctx.executionContext().executor().execute(() -> { + ctx.executionContext().executor().submit(() -> { int receivedSize = 0; while (iterator.hasNext()) { Buffer chunk = iterator.next(); @@ -196,7 +200,11 @@ public Completable accept(final ConnectionContext context) { receivedSize += chunk.readableBytes(); } serverReceivedRequestPayload.add(receivedSize); - }); + }).beforeOnError(cause -> { + LOGGER.error("failure while writing response", cause); + serverReceivedRequestPayload.add(-1); + PayloadWriterUtils.safeClose(writer, cause); + }).toFuture(); serverSendResponsePayload.await(); writer.write(RESPONSE_CONTENT); } @@ -474,22 +482,21 @@ void closePipelinedAfterTwoRequestsSentBeforeAnyResponseReceived(HttpProtocol pr } private StreamingHttpRequest newRequest(String path) { - return connection.asConnection().post(path) - .addHeader(CONTENT_LENGTH, valueOf(REQUEST_CONTENT.length())) - .payloadBody(REQUEST_CONTENT, textSerializer()) - .toStreamingRequest(); + return connection.post(path) + .addHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(REQUEST_CONTENT.length()))) + .payloadBody(from(REQUEST_CONTENT), appSerializerUtf8FixLen()); } private StreamingHttpRequest newRequest(String path, CountDownLatch payloadBodyLatch) { return connection.post(path) - .addHeader(CONTENT_LENGTH, valueOf(REQUEST_CONTENT.length())) + .addHeader(CONTENT_LENGTH, valueOf(addFixedLengthFramingOverhead(REQUEST_CONTENT.length()))) .payloadBody(connection.connectionContext().executionContext().executor().submit(() -> { try { payloadBodyLatch.await(); } catch (InterruptedException e) { throwException(e); } - }).concat(from(REQUEST_CONTENT)), textSerializer()); + }).concat(from(REQUEST_CONTENT)), appSerializerUtf8FixLen()); } private static void assertResponse(StreamingHttpResponse response) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java index a6d55d59e5..fec7a0404a 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2PriorKnowledgeFeatureParityTest.java @@ -115,7 +115,6 @@ import static io.servicetalk.buffer.api.Matchers.contentEqualTo; import static io.servicetalk.concurrent.api.Completable.completed; import static io.servicetalk.concurrent.api.Processors.newPublisherProcessor; -import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.api.SourceAdapters.fromSource; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; @@ -132,8 +131,7 @@ import static io.servicetalk.http.api.HttpResponseStatus.EXPECTATION_FAILED; import static io.servicetalk.http.api.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; @@ -257,7 +255,7 @@ public Single handle(final HttpServiceContext ctx, .executionStrategy(clientExecutionStrategy).buildBlocking()) { HttpResponse response = client.request(client.newRequest(method, "/p") .addQueryParameters(qpName, "bar")) - .payloadBody(responseBody, textSerializer()); + .payloadBody(responseBody, textSerializerUtf8()); assertThat("Unexpected response status.", response.status(), equalTo(OK)); } } @@ -271,8 +269,8 @@ private void multipleRequests(boolean get, int numberRequests) throws Exception .executionStrategy(clientExecutionStrategy).buildBlocking()) { for (int i = 0; i < numberRequests; ++i) { HttpResponse response = client.request((get ? client.get("/" + i) : client.post("/" + i)) - .payloadBody(responseBody, textSerializer())); - assertEquals(responseBody, response.payloadBody(textDeserializer())); + .payloadBody(responseBody, textSerializerUtf8())); + assertEquals(responseBody, response.payloadBody(textSerializerUtf8())); } } } @@ -468,8 +466,8 @@ void serverHeaderCookieRemovalAndIteration(HttpTestExecutionStrategy strategy, try (BlockingHttpClient client = forSingleAddress(HostAndPort.of(serverAddress)) .protocols(h2PriorKnowledge ? h2Default() : h1Default()) .executionStrategy(clientExecutionStrategy).buildBlocking()) { - assertThat(client.request(client.get("/").payloadBody("", textSerializer())) - .payloadBody(textDeserializer()), isEmptyString()); + assertThat(client.request(client.get("/").payloadBody("", textSerializerUtf8())) + .payloadBody(textSerializerUtf8()), isEmptyString()); } } @@ -622,7 +620,7 @@ protected Single request(final StreamingHttpRequester del }).flatMap(req -> delegate.request(strategy, req)); } }).buildBlocking()) { - HttpRequest request = client.get("/").payloadBody("a", textSerializer()); + HttpRequest request = client.get("/").payloadBody("a", textSerializerUtf8()); if (addTrailers) { request.trailers().set("mytrailer", "myvalue"); } @@ -793,7 +791,7 @@ public Single handle(final HttpServiceContext ctx, .protocols(h2PriorKnowledge ? h2Default() : h1Default()) .executionStrategy(clientExecutionStrategy) .buildBlocking()) { - HttpRequest request = client.get("/").payloadBody("a", textSerializer()); + HttpRequest request = client.get("/").payloadBody("a", textSerializerUtf8()); if (addTrailers) { request.trailers().set("mytrailer", "myvalue"); } @@ -949,8 +947,8 @@ void serverHeaderSetCookieRemovalAndIteration(HttpTestExecutionStrategy strategy try (BlockingHttpClient client = forSingleAddress(HostAndPort.of(serverAddress)) .protocols(h2PriorKnowledge ? h2Default() : h1Default()) .executionStrategy(clientExecutionStrategy).buildBlocking()) { - assertThat(client.request(client.get("/").payloadBody("", textSerializer())) - .payloadBody(textDeserializer()), isEmptyString()); + assertThat(client.request(client.get("/").payloadBody("", textSerializerUtf8())) + .payloadBody(textSerializerUtf8()), isEmptyString()); } } @@ -1026,10 +1024,10 @@ void clientReserveConnectionMultipleRequests(HttpTestExecutionStrategy strategy, try { // We interleave the requests intentionally to make sure the internal transport sequences the // reads and writes correctly. - HttpResponse response1 = client.request(request.payloadBody(responseBody1, textSerializer())); - HttpResponse response2 = client.request(request.payloadBody(responseBody2, textSerializer())); - assertEquals(responseBody1, response1.payloadBody(textDeserializer())); - assertEquals(responseBody2, response2.payloadBody(textDeserializer())); + HttpResponse response1 = client.request(request.payloadBody(responseBody1, textSerializerUtf8())); + HttpResponse response2 = client.request(request.payloadBody(responseBody2, textSerializerUtf8())); + assertEquals(responseBody1, response1.payloadBody(textSerializerUtf8())); + assertEquals(responseBody2, response2.payloadBody(textSerializerUtf8())); } finally { reservedConnection.release(); } @@ -1062,7 +1060,7 @@ void serverWriteTrailers(HttpTestExecutionStrategy strategy, try (BlockingHttpClient client = forSingleAddress(HostAndPort.of(serverAddress)) .protocols(h2PriorKnowledge ? h2Default() : h1Default()) .executionStrategy(clientExecutionStrategy).buildBlocking()) { - HttpRequest request = client.post("/").payloadBody(payloadBody, textSerializer()); + HttpRequest request = client.post("/").payloadBody(payloadBody, textSerializerUtf8()); HttpResponse response = client.request(request); assertEquals(0, response.payloadBody().readableBytes()); CharSequence responseTrailer = response.trailers().get(myTrailerName); @@ -1083,10 +1081,10 @@ void clientWriteTrailers(HttpTestExecutionStrategy strategy, String payloadBody = "foo"; String myTrailerName = "mytrailer"; String myTrailerValue = "myvalue"; - HttpRequest request = client.post("/").payloadBody(payloadBody, textSerializer()); + HttpRequest request = client.post("/").payloadBody(payloadBody, textSerializerUtf8()); request.trailers().add(myTrailerName, myTrailerValue); HttpResponse response = client.request(request); - assertEquals(payloadBody, response.payloadBody(textDeserializer())); + assertEquals(payloadBody, response.payloadBody(textSerializerUtf8())); CharSequence responseTrailer = response.trailers().get(myTrailerName); assertNotNull(responseTrailer); assertEquals(0, responseTrailer.toString().compareToIgnoreCase(myTrailerValue)); @@ -1115,8 +1113,8 @@ protected Single request(final StreamingHttpRequester del final String responseBody = "foo"; HttpResponse response = client.request(client.post("/0") - .payloadBody(responseBody, textSerializer())); - assertEquals(responseBody, response.payloadBody(textDeserializer())); + .payloadBody(responseBody, textSerializerUtf8())); + assertEquals(responseBody, response.payloadBody(textSerializerUtf8())); assertNoAsyncErrors(errorQueue); } } @@ -1141,8 +1139,8 @@ public Single request(final HttpExecutionStrategy strateg .buildBlocking()) { final String responseBody = "foo"; - HttpResponse response = client.request(client.post("/0").payloadBody(responseBody, textSerializer())); - assertEquals(responseBody, response.payloadBody(textDeserializer())); + HttpResponse response = client.request(client.post("/0").payloadBody(responseBody, textSerializerUtf8())); + assertEquals(responseBody, response.payloadBody(textSerializerUtf8())); assertNoAsyncErrors(errorQueue); } } @@ -1395,7 +1393,7 @@ void trailersWithContentLength(HttpTestExecutionStrategy strategy, return responseFactory.ok() .addTrailer(expectedTrailer, expectedTrailerValue) .addHeader(CONTENT_LENGTH, expectedPayloadLength) - .payloadBody(expectedPayload, textSerializer()); + .payloadBody(expectedPayload, textSerializerUtf8()); }); BlockingHttpClient client = forSingleAddress(HostAndPort.of( (InetSocketAddress) serverContext.listenAddress())) @@ -1405,15 +1403,15 @@ void trailersWithContentLength(HttpTestExecutionStrategy strategy, HttpResponse response = client.request(client.post("/") .addTrailer(expectedTrailer, expectedTrailerValue) .addHeader(CONTENT_LENGTH, expectedPayloadLength) - .payloadBody(expectedPayload, textSerializer())); + .payloadBody(expectedPayload, textSerializerUtf8())); assertThat(response.status(), is(OK)); - assertThat(response.payloadBody(textDeserializer()), equalTo(expectedPayload)); + assertThat(response.payloadBody(textSerializerUtf8()), equalTo(expectedPayload)); assertHeaders(h2PriorKnowledge, response.headers(), expectedPayloadLength); assertTrailers(response.trailers(), expectedTrailer, expectedTrailerValue); // Verify what server received: HttpRequest request = requestReceived.get(); - assertThat(request.payloadBody(textDeserializer()), equalTo(expectedPayload)); + assertThat(request.payloadBody(textSerializerUtf8()), equalTo(expectedPayload)); assertHeaders(h2PriorKnowledge, request.headers(), expectedPayloadLength); assertTrailers(request.trailers(), expectedTrailer, expectedTrailerValue); } @@ -1572,8 +1570,10 @@ private InetSocketAddress bindHttpSynchronousResponseServer(Consumer + resp.payloadBody(throwableToString(cause), textSerializerUtf8()) + .toStreamingResponse()); } StreamingHttpResponse resp = responseFactory.ok(); CharSequence contentType = request.headers().get(CONTENT_TYPE); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2ResponseCancelTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2ResponseCancelTest.java index f4fa687595..24d02ba465 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2ResponseCancelTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/H2ResponseCancelTest.java @@ -50,7 +50,7 @@ import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_2_0; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED_SERVER; import static io.servicetalk.http.netty.HttpProtocol.HTTP_2; @@ -145,7 +145,7 @@ void testServerClosesStreamNotConnection() throws Exception { assertThat(e.getCause(), instanceOf(H2StreamResetException.class)); // Mak sure we can use the same connection for future requests: - assertResponse(makeRequest(newRequest(connection, "ok")), HTTP_2_0, OK, "ok"); + assertSerializedResponse(makeRequest(newRequest(connection, "ok")), HTTP_2_0, OK, "ok"); assertThat("Connection closed unexpectedly", connectionClosed.get(), is(false)); } @@ -176,14 +176,14 @@ private void requestCancellationResetsStreamButNotParentConnection(StreamingHttp assertThat("Unexpected responses", responses, is(empty())); firstResponseLatch.countDown(); - assertResponse(responses.take(), HTTP_2_0, OK, "first"); + assertSerializedResponse(responses.take(), HTTP_2_0, OK, "first"); // Make sure we can use the same connection for future requests: - assertResponse(makeRequest(newRequest(requester, "third")), HTTP_2_0, OK, "third"); + assertSerializedResponse(makeRequest(newRequest(requester, "third")), HTTP_2_0, OK, "third"); } private static StreamingHttpRequest newRequest(StreamingHttpRequestFactory requestFactory, String param) { return requestFactory.post(SVC_ECHO) .addQueryParameter(PARAM, param) - .payloadBody(from(param), textSerializer()); + .payloadBody(from(param), appSerializerUtf8FixLen()); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HostHeaderHttpRequesterFilterTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HostHeaderHttpRequesterFilterTest.java index f86bc5c6e7..1720456625 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HostHeaderHttpRequesterFilterTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HostHeaderHttpRequesterFilterTest.java @@ -32,8 +32,7 @@ import static io.servicetalk.http.api.HttpExecutionStrategies.noOffloadsStrategy; import static io.servicetalk.http.api.HttpHeaderNames.HOST; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; @@ -119,7 +118,7 @@ private ServerContext buildServer() throws Exception { final CharSequence host = request.headers().get(HOST); return responseFactory.ok() .version(httpVersionConfig.version()) - .payloadBody(host != null ? host.toString() : "null", textSerializer()); + .payloadBody(host != null ? host.toString() : "null", textSerializerUtf8()); }); } @@ -191,7 +190,7 @@ private void assertResponse(BlockingHttpRequester requester, @Nullable String ho assertThat(response.status(), equalTo(OK)); assertThat(response.version(), equalTo(httpVersionConfig.version())); // "Host" header is not required for HTTP/1.0. Therefore, we may expect "null" here. - assertThat(response.payloadBody(textDeserializer()), equalTo( + assertThat(response.payloadBody(textSerializerUtf8()), equalTo( httpVersionConfig == HttpVersionConfig.HTTP_1_0 && hostHeader == null ? "null" : expectedValue)); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextProtocolTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextProtocolTest.java index c8d6726325..ebb1ebf817 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextProtocolTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextProtocolTest.java @@ -32,8 +32,7 @@ import java.net.InetSocketAddress; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; import static io.servicetalk.http.netty.HttpProtocolConfigs.h2Default; import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname; @@ -76,7 +75,7 @@ void testProtocol(Config config) throws Exception { assertThat("Client-side connection protocol does not match expected value", connection.connectionContext().protocol(), equalTo(config.expectedProtocol)); assertThat("Server-side connection protocol does not match expected value", - connection.request(connection.get("/")).payloadBody(textDeserializer()), + connection.request(connection.get("/")).payloadBody(textSerializerUtf8()), equalTo(config.expectedProtocol.name())); } } @@ -89,7 +88,7 @@ private static ServerContext startServer(Config config) throws Exception { DefaultTestCerts::loadServerKey).build()); } return builder.listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok() - .payloadBody(ctx.protocol().name(), textSerializer())); + .payloadBody(ctx.protocol().name(), textSerializerUtf8())); } private static BlockingHttpClient newClient(ServerContext serverContext, Config config) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextSocketOptionTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextSocketOptionTest.java index e1a8904eca..9add7ebd4d 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextSocketOptionTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpConnectionContextSocketOptionTest.java @@ -32,8 +32,7 @@ import java.net.StandardSocketOptions; import javax.annotation.Nullable; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static java.lang.String.valueOf; @@ -101,7 +100,7 @@ private void testSocketOption(SocketOption socketOption, Matcher assertThat("Client-side connection SocketOption does not match expected value", connection.connectionContext().socketOption(socketOption), clientMatcher); assertThat("Server-side connection SocketOption does not match expected value", - connection.request(connection.get("/")).payloadBody(textDeserializer()), serverMatcher); + connection.request(connection.get("/")).payloadBody(textSerializerUtf8()), serverMatcher); } } @@ -113,7 +112,7 @@ private ServerContext startServer(@Nullable Long idleTimeoutMs, SocketOption builder.socketOption(ServiceTalkSocketOptions.IDLE_TIMEOUT, idleTimeoutMs); } return builder.listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok() - .payloadBody(valueOf(ctx.socketOption(socketOption)), textSerializer())); + .payloadBody(valueOf(ctx.socketOption(socketOption)), textSerializerUtf8())); } private BlockingHttpClient newClient(ServerContext serverContext, @Nullable Long idleTimeoutMs, @@ -136,7 +135,7 @@ void unsupportedSocketOptionThrows(HttpProtocol protocol) throws Exception { .listenBlockingAndAwait((ctx, request, responseFactory) -> { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ctx.socketOption(unsupported)); - return responseFactory.ok().payloadBody(ex.getMessage(), textSerializer()); + return responseFactory.ok().payloadBody(ex.getMessage(), textSerializerUtf8()); }); BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) .protocols(protocol.config).buildBlocking(); @@ -146,7 +145,7 @@ void unsupportedSocketOptionThrows(HttpProtocol protocol) throws Exception { () -> connection.connectionContext().socketOption(unsupported)); assertThat(ex.getMessage(), endsWith("not supported")); assertThat(ex.getMessage(), - equalTo(connection.request(connection.get("/")).payloadBody(textDeserializer()))); + equalTo(connection.request(connection.get("/")).payloadBody(textSerializerUtf8()))); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProxyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProxyTest.java index 29caf9dfa3..86310b32b6 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProxyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpProxyTest.java @@ -31,7 +31,7 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.api.HttpHeaderNames.HOST; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpsProxyTest.safeClose; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; @@ -83,7 +83,7 @@ void startProxy() throws Exception { void startServer() throws Exception { serverContext = HttpServers.forAddress(localAddress(0)) .listenAndAwait((ctx, request, responseFactory) -> succeeded(responseFactory.ok() - .payloadBody("host: " + request.headers().get(HOST), textSerializer()))); + .payloadBody("host: " + request.headers().get(HOST), textSerializerUtf8()))); serverAddress = serverHostAndPort(serverContext); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpRawDataSerializationTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpRawDataSerializationTest.java new file mode 100644 index 0000000000..2588b8765c --- /dev/null +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpRawDataSerializationTest.java @@ -0,0 +1,80 @@ +/* + * Copyright © 2021 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.http.netty; + +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.HttpStreamingSerializer; +import io.servicetalk.transport.api.ServerContext; + +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static io.servicetalk.concurrent.api.Publisher.from; +import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; +import static io.servicetalk.http.api.HttpSerializers.bytesStreamingSerializer; +import static io.servicetalk.http.api.HttpSerializers.stringStreamingSerializer; +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * This test simulates pre-existing content in string, and we know the content type a-prior. This maybe useful when + * calling 3rd party libraries that provide json/xml content in String. + */ +class HttpRawDataSerializationTest { + @Test + void stringSerialization() throws Exception { + final String contentType = "foo"; + final String[] content = {"hello", "world", "!"}; + int expectedContentLength = 0; + for (final String s : content) { + expectedContentLength += s.length(); + } + + runTest(content, contentType, expectedContentLength, stringStreamingSerializer(UTF_8, + headers -> headers.set(CONTENT_TYPE, contentType))); + } + + @Test + void bytesSerialization() throws Exception { + final String contentType = "foo"; + final byte[][] content = {"hello".getBytes(UTF_8), "world".getBytes(UTF_8), "!".getBytes(UTF_8)}; + int expectedContentLength = 0; + for (final byte[] s : content) { + expectedContentLength += s.length; + } + + runTest(content, contentType, expectedContentLength, bytesStreamingSerializer( + headers -> headers.set(CONTENT_TYPE, contentType))); + } + + private static void runTest(T[] content, String contentType, int expectedContentLength, + HttpStreamingSerializer streamingSerializer) throws Exception { + try (ServerContext srv = HttpServers.forAddress(localAddress(0)) + .listenStreamingAndAwait((ctx, request, responseFactory) -> + succeeded(responseFactory.ok().payloadBody(from(content), streamingSerializer))); + BlockingHttpClient clt = HttpClients.forSingleAddress(serverHostAndPort(srv)).buildBlocking()) { + HttpResponse resp = clt.request(clt.get("/hello")); + assertThat(Objects.toString(resp.headers().get(CONTENT_TYPE)), is(contentType)); + assertThat(resp.payloadBody().readableBytes(), is(expectedContentLength)); + } + } +} diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializationErrorTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializationErrorTest.java index 27ded9ea58..992e308e3d 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializationErrorTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializationErrorTest.java @@ -16,16 +16,14 @@ package io.servicetalk.http.netty; import io.servicetalk.concurrent.api.Executor; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.http.api.BlockingHttpClient; -import io.servicetalk.http.api.HttpDeserializer; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; -import io.servicetalk.http.api.HttpSerializer; -import io.servicetalk.serialization.api.TypeHolder; +import io.servicetalk.http.api.HttpSerializerDeserializer; +import io.servicetalk.http.api.HttpStreamingSerializerDeserializer; import io.servicetalk.transport.api.ServerContext; +import com.fasterxml.jackson.core.type.TypeReference; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -34,8 +32,10 @@ import java.util.Map; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; import static io.servicetalk.http.api.HttpResponseStatus.INTERNAL_SERVER_ERROR; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; +import static io.servicetalk.http.api.HttpSerializers.jsonSerializer; +import static io.servicetalk.http.api.HttpSerializers.jsonStreamingSerializer; import static io.servicetalk.http.netty.HttpTestExecutionStrategy.CACHED; import static io.servicetalk.http.netty.HttpTestExecutionStrategy.NO_OFFLOAD; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; @@ -44,6 +44,7 @@ import static java.util.Collections.emptyMap; import static org.junit.jupiter.api.Assertions.assertEquals; +@Deprecated class HttpSerializationErrorTest { private HttpExecutionStrategy serverExecutionStrategy; @@ -63,10 +64,11 @@ void teardown() throws Exception { @MethodSource("executors") void serializationMapThrowsPropagatesToClient(HttpTestExecutionStrategy serverStrategy) throws Exception { serverExecutionStrategy = serverStrategy.executorSupplier.get(); - HttpSerializationProvider jackson = jsonSerializer(new JacksonSerializationProvider()); - TypeHolder> mapType = new TypeHolder>() { }; - HttpSerializer> serializer = jackson.serializerFor(mapType); - HttpDeserializer> deserializer = jackson.deserializerFor(mapType); + TypeReference> mapType = new TypeReference>() { }; + HttpSerializerDeserializer> httpSerializer = + jsonSerializer(JACKSON.serializerDeserializer(mapType)); + HttpStreamingSerializerDeserializer> httpStreamingSerializer = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(mapType)); try (ServerContext srv = HttpServers.forAddress(localAddress(0)) .executionStrategy(serverExecutionStrategy) // We build an aggregated service, but convert to/from the streaming API so that we can easily throw @@ -74,12 +76,12 @@ void serializationMapThrowsPropagatesToClient(HttpTestExecutionStrategy serverSt // hanging. .listenAndAwait((ctx, request, responseFactory) -> responseFactory.ok().toStreamingResponse().payloadBody( - request.toStreamingRequest().payloadBody(deserializer).map(result -> { + request.toStreamingRequest().payloadBody(httpStreamingSerializer).map(result -> { throw DELIBERATE_EXCEPTION; - }), serializer).toResponse()); + }), httpStreamingSerializer).toResponse()); BlockingHttpClient clt = HttpClients.forSingleAddress(serverHostAndPort(srv)).buildBlocking()) { - HttpResponse resp = clt.request(clt.post("/foo").payloadBody(emptyMap(), serializer)); + HttpResponse resp = clt.request(clt.post("/foo").payloadBody(emptyMap(), httpSerializer)); assertEquals(INTERNAL_SERVER_ERROR, resp.status()); } } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializerErrorTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializerErrorTest.java new file mode 100644 index 0000000000..0501e3f7ba --- /dev/null +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpSerializerErrorTest.java @@ -0,0 +1,145 @@ +/* + * Copyright © 2019, 2021 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.http.netty; + +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Executor; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.HttpExecutionStrategy; +import io.servicetalk.http.api.HttpPayloadWriter; +import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.HttpSerializerDeserializer; +import io.servicetalk.http.api.HttpSerializers; +import io.servicetalk.http.api.HttpStreamingSerializerDeserializer; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.transport.api.ServerContext; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Collection; +import java.util.Map; + +import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; +import static io.servicetalk.http.api.HttpResponseStatus.BAD_REQUEST; +import static io.servicetalk.http.api.HttpResponseStatus.INTERNAL_SERVER_ERROR; +import static io.servicetalk.http.api.HttpSerializers.jsonStreamingSerializer; +import static io.servicetalk.http.netty.HttpTestExecutionStrategy.CACHED; +import static io.servicetalk.http.netty.HttpTestExecutionStrategy.NO_OFFLOAD; +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HttpSerializerErrorTest { + private HttpExecutionStrategy serverExecutionStrategy; + + static Collection executors() { + return asList(NO_OFFLOAD, CACHED); + } + + @AfterEach + void teardown() throws Exception { + Executor executor = serverExecutionStrategy.executor(); + if (executor != null) { + executor.closeAsync().toFuture().get(); + } + } + + @ParameterizedTest + @MethodSource("executors") + void blockingStreamingDeserializationHeaderMismatch(HttpTestExecutionStrategy serverStrategy) throws Exception { + serverExecutionStrategy = serverStrategy.executorSupplier.get(); + HttpStreamingSerializerDeserializer streamingSerializer = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(String.class)); + try (ServerContext srv = HttpServers.forAddress(localAddress(0)) + .executionStrategy(serverExecutionStrategy) + .listenBlockingStreamingAndAwait((ctx, request, responseFactory) -> { + try { + BlockingIterable reqIterable = request.payloadBody(streamingSerializer); + try (HttpPayloadWriter stream = responseFactory.sendMetaData(streamingSerializer)) { + for (String reqChunk : reqIterable) { + stream.write(reqChunk); + } + } + } catch (SerializationException e) { + responseFactory.status(BAD_REQUEST); + responseFactory.sendMetaData().close(); + } + }); + BlockingHttpClient clt = HttpClients.forSingleAddress(serverHostAndPort(srv)).buildBlocking()) { + + HttpResponse resp = clt.request(clt.post("/foo").payloadBody( + clt.executionContext().bufferAllocator().fromAscii("hello"))); + assertEquals(BAD_REQUEST, resp.status()); + } + } + + @ParameterizedTest + @MethodSource("executors") + void streamingDeserializationHeaderMismatch(HttpTestExecutionStrategy serverStrategy) throws Exception { + serverExecutionStrategy = serverStrategy.executorSupplier.get(); + HttpStreamingSerializerDeserializer streamingSerializer = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(String.class)); + try (ServerContext srv = HttpServers.forAddress(localAddress(0)) + .executionStrategy(serverExecutionStrategy) + .listenStreamingAndAwait((ctx, request, responseFactory) -> { + try { + return succeeded(responseFactory.ok().payloadBody( + request.payloadBody(streamingSerializer), streamingSerializer)); + } catch (SerializationException e) { + return succeeded(responseFactory.badRequest()); + } + }); + BlockingHttpClient clt = HttpClients.forSingleAddress(serverHostAndPort(srv)).buildBlocking()) { + + HttpResponse resp = clt.request(clt.post("/foo").payloadBody( + clt.executionContext().bufferAllocator().fromAscii("hello"))); + assertEquals(BAD_REQUEST, resp.status()); + } + } + + @ParameterizedTest + @MethodSource("executors") + void serializationMapThrowsPropagatesToClient(HttpTestExecutionStrategy serverStrategy) throws Exception { + serverExecutionStrategy = serverStrategy.executorSupplier.get(); + TypeReference> mapType = new TypeReference>() { }; + HttpStreamingSerializerDeserializer> streamingSerializer = + jsonStreamingSerializer(JACKSON.streamingSerializerDeserializer(mapType)); + HttpSerializerDeserializer> serializer = + HttpSerializers.jsonSerializer(JACKSON.serializerDeserializer(mapType)); + try (ServerContext srv = HttpServers.forAddress(localAddress(0)) + .executionStrategy(serverExecutionStrategy) + // We build an aggregated service, but convert to/from the streaming API so that we can easily throw + // and exception when the entire request is available and follow the control flow that was previously + // hanging. + .listenAndAwait((ctx, request, responseFactory) -> + responseFactory.ok().toStreamingResponse().payloadBody( + request.toStreamingRequest().payloadBody(streamingSerializer).map(result -> { + throw DELIBERATE_EXCEPTION; + }), streamingSerializer).toResponse()); + BlockingHttpClient clt = HttpClients.forSingleAddress(serverHostAndPort(srv)).buildBlocking()) { + + HttpResponse resp = clt.request(clt.post("/foo").payloadBody(emptyMap(), serializer)); + assertEquals(INTERNAL_SERVER_ERROR, resp.status()); + } + } +} diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpsProxyTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpsProxyTest.java index 1ea3d5f956..0ca113bb59 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpsProxyTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/HttpsProxyTest.java @@ -34,7 +34,7 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.api.HttpHeaderNames.HOST; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname; import static io.servicetalk.transport.netty.NettyIoExecutors.createIoExecutor; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; @@ -95,7 +95,7 @@ void startServer() throws Exception { .sslConfig(new ServerSslConfigBuilder(DefaultTestCerts::loadServerPem, DefaultTestCerts::loadServerKey).build()) .listenAndAwait((ctx, request, responseFactory) -> succeeded(responseFactory.ok() - .payloadBody("host: " + request.headers().get(HOST), textSerializer()))); + .payloadBody("host: " + request.headers().get(HOST), textSerializerUtf8()))); serverAddress = serverHostAndPort(serverContext); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/MalformedDataAfterHttpMessageTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/MalformedDataAfterHttpMessageTest.java index 9afcc4a35a..8bfc0f73ee 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/MalformedDataAfterHttpMessageTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/MalformedDataAfterHttpMessageTest.java @@ -48,8 +48,7 @@ import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.http.netty.HttpServers.forAddress; import static io.servicetalk.logging.api.LogLevel.TRACE; @@ -93,7 +92,7 @@ void afterResponse() throws Exception { HttpResponse response = connection.request(connection.get("/")); assertThat(response.status(), is(OK)); assertThat(response.headers().get(CONTENT_LENGTH), contentEqualTo(valueOf(CONTENT.length()))); - assertThat(response.payloadBody(textDeserializer()), equalTo(CONTENT)); + assertThat(response.payloadBody(textSerializerUtf8()), equalTo(CONTENT)); // Verify that the next request fails and connection gets closed: assertThrows(DecoderException.class, () -> connection.request(connection.get("/"))); @@ -120,7 +119,7 @@ void afterRequest() throws Exception { .payloadBody(malformedBody)); assertThat(response.status(), is(OK)); assertThat(response.headers().get(CONTENT_LENGTH), contentEqualTo(valueOf(CONTENT.length()))); - assertThat(response.payloadBody(textDeserializer()), equalTo(CONTENT)); + assertThat(response.payloadBody(textSerializerUtf8()), equalTo(CONTENT)); // Server should close the connection: connectionClosedLatch.await(); @@ -158,8 +157,8 @@ private static ServerContext stServer() throws Exception { .bufferAllocator(SERVER_CTX.bufferAllocator()) .enableWireLogging("servicetalk-tests-wire-logger", TRACE, () -> true) .listenBlockingAndAwait((ctx, request, responseFactory) -> - responseFactory.ok() - .payloadBody(request.payloadBody(textDeserializer()), textSerializer())); + responseFactory.ok().payloadBody(request.payloadBody(textSerializerUtf8()), + textSerializerUtf8())); } private static BlockingHttpClient stClient(SocketAddress serverAddress) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/NettyHttpServerConnectionDrainTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/NettyHttpServerConnectionDrainTest.java index c66b38aa8c..3acf37acac 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/NettyHttpServerConnectionDrainTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/NettyHttpServerConnectionDrainTest.java @@ -40,8 +40,8 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.internal.FutureUtils.awaitTermination; import static io.servicetalk.concurrent.internal.TestTimeoutConstants.CI; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.MatcherAssert.assertThat; @@ -78,7 +78,7 @@ void requestIsAutoDrainedWhenUserFailsToConsume() throws Exception { void requestIsDrainedByUserWithDrainingDisabled() throws Exception { try (ServerContext serverContext = server(false, (ctx, request, responseFactory) -> request.messageBody().ignoreElements() // User consumes payload (ignoring) - .concat(succeeded(responseFactory.ok().payloadBody(from("OK"), textSerializer())))); + .concat(succeeded(responseFactory.ok().payloadBody(from("OK"), appSerializerUtf8FixLen())))); BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) .buildBlocking()) { @@ -97,7 +97,7 @@ void requestIsConsumedByUserWithDrainingEnabled() throws Exception { .map(StringBuilder::toString) .whenOnSuccess(resultRef::set) .toCompletable() - .concat(succeeded(responseFactory.ok().payloadBody(from("OK"), textSerializer())))); + .concat(succeeded(responseFactory.ok().payloadBody(from("OK"), appSerializerUtf8FixLen())))); BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) .buildBlocking()) { @@ -116,12 +116,12 @@ void requestTimesOutWithoutAutoDrainingOrUserConsuming() throws Exception { client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)).buildStreaming(); - client.request(client.post("/").payloadBody(from(LARGE_TEXT), textSerializer())) + client.request(client.post("/").payloadBody(from(LARGE_TEXT), appSerializerUtf8FixLen())) // Subscribe to send the request, don't care about the response since graceful close of the server // will hang until the request is consumed, thus we expect the timeout to hit .ignoreElement().subscribe(); - assertThrows(TimeoutException.class, () -> latch.await()); // Wait till the request is received + assertThrows(TimeoutException.class, latch::await); // Wait till the request is received // before initiating graceful close of the server } finally { closeClient(client); @@ -135,14 +135,15 @@ private static void closeClient(@Nullable final AutoCloseable client) throws Exc } private static void postLargePayloadAndAssertResponseOk(final BlockingHttpClient client) throws Exception { - HttpResponse response = client.request(client.post("/").payloadBody(LARGE_TEXT, textSerializer())); - assertThat(response.payloadBody(textDeserializer()), equalTo("OK")); + HttpResponse response = client.request(client.post("/").payloadBody(LARGE_TEXT, textSerializerUtf8())); + assertThat(response.toStreamingResponse().payloadBody(appSerializerUtf8FixLen()) + .collect(StringBuilder::new, StringBuilder::append).toFuture().get().toString(), equalTo("OK")); } private static StreamingHttpService respondOkWithoutReadingRequest(Runnable onRequest) { return (ctx, request, responseFactory) -> { onRequest.run(); - return succeeded(responseFactory.ok().payloadBody(from("OK"), textSerializer())); + return succeeded(responseFactory.ok().payloadBody(from("OK"), appSerializerUtf8FixLen())); }; } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PartitionedHttpClientTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PartitionedHttpClientTest.java index edc42299a5..5e25319ccf 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PartitionedHttpClientTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PartitionedHttpClientTest.java @@ -56,7 +56,7 @@ import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static java.net.InetAddress.getLoopbackAddress; @@ -67,9 +67,6 @@ import static org.mockito.Mockito.when; class PartitionedHttpClientTest { - - - private static final PartitionAttributes.Key SRV_NAME = PartitionAttributes.Key.newKey(); private static final PartitionAttributes.Key SRV_LEADER = PartitionAttributes.Key.newKey(); private static final String SRV_1 = "srv1"; @@ -264,7 +261,7 @@ void testClientGroupPartitioning() throws Exception { } ServerContext dSrv = userId == 1 ? srv1 : srv2; InetSocketAddress socketAddress = (InetSocketAddress) dSrv.listenAddress(); - return responseFactory.ok().payloadBody(socketAddress.getPort() + "", textSerializer()); + return responseFactory.ok().payloadBody(socketAddress.getPort() + "", textSerializerUtf8()); } return responseFactory.notFound(); })) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PayloadBodyModificationsTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PayloadBodyModificationsTest.java index d37adc099b..76a5c0a08a 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PayloadBodyModificationsTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/PayloadBodyModificationsTest.java @@ -25,7 +25,7 @@ import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED; import static io.servicetalk.http.netty.AbstractNettyHttpServerTest.ExecutorSupplier.CACHED_SERVER; import static io.servicetalk.http.netty.TestServiceStreaming.SVC_ECHO; @@ -46,13 +46,6 @@ void aggregatedSetPayloadBody() throws Exception { assertResponse(makeRequest(request.toStreamingRequest()), request.version(), OK, CONTENT); } - @Test - void aggregatedSetPayloadBodyWithSerializer() throws Exception { - HttpRequest request = streamingHttpClient().asClient().post(SVC_ECHO) - .payloadBody(CONTENT, textSerializer()); - assertResponse(makeRequest(request.toStreamingRequest()), request.version(), OK, CONTENT); - } - @Test void aggregatedExpandOriginalBuffer() throws Exception { HttpRequest request = streamingHttpClient().asClient().post(SVC_ECHO); @@ -91,8 +84,8 @@ void streamingSetPayloadBody() throws Exception { @Test void streamingSetPayloadBodyWithSerializer() throws Exception { StreamingHttpRequest request = streamingHttpClient().post(SVC_ECHO) - .payloadBody(from(CONTENT), textSerializer()); - assertResponse(makeRequest(request), request.version(), OK, CONTENT); + .payloadBody(from(CONTENT), appSerializerUtf8FixLen()); + assertSerializedResponse(makeRequest(request), request.version(), OK, CONTENT); } @Test diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SecurityHandshakeObserverTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SecurityHandshakeObserverTest.java index 31d23f2afd..084f0785df 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SecurityHandshakeObserverTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SecurityHandshakeObserverTest.java @@ -46,8 +46,7 @@ import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.http.netty.HttpClients.forSingleAddressViaProxy; import static io.servicetalk.http.netty.HttpProtocolConfigs.h1Default; @@ -177,9 +176,9 @@ private void verifyHandshakeObserved(Function .buildBlocking()) { String content = "payload_body"; - HttpResponse response = client.request(client.post(SVC_ECHO).payloadBody(content, textSerializer())); + HttpResponse response = client.request(client.post(SVC_ECHO).payloadBody(content, textSerializerUtf8())); assertThat(response.status(), is(OK)); - assertThat(response.payloadBody(textDeserializer()), equalTo(content)); + assertThat(response.payloadBody(textSerializerUtf8()), equalTo(content)); verify(clientConnectionObserver).onSecurityHandshake(); verify(clientSecurityHandshakeObserver).handshakeComplete(any(SSLSession.class)); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerGracefulConnectionClosureHandlingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerGracefulConnectionClosureHandlingTest.java index 50edbde9fc..ffa0d0d858 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerGracefulConnectionClosureHandlingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerGracefulConnectionClosureHandlingTest.java @@ -39,7 +39,7 @@ import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.noOffloadsStrategy; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_LENGTH; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.netty.HttpServers.forAddress; import static io.servicetalk.http.netty.NettyHttpServer.NettyHttpServerConnection; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; @@ -85,7 +85,7 @@ public Completable accept(final ConnectionContext context) { .payloadBody( request.payloadBody().ignoreElements() .concat(from(RESPONSE_CONTENT)), - textSerializer()) + appSerializerUtf8FixLen()) // Close ServerContext after response is complete .transformMessageBody(payload -> payload .whenFinally(serverClose.get())))); @@ -118,7 +118,7 @@ void test() throws Exception { while (in.read() >= 0) { total++; } - assertThat(total, is(96)); + assertThat(total, is(114)); } awaitServerConnectionClosed(); diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerRespondsOnClosingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerRespondsOnClosingTest.java index 1be298e461..9ae868d6c9 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerRespondsOnClosingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServerRespondsOnClosingTest.java @@ -46,7 +46,7 @@ import static io.servicetalk.http.api.HttpHeaderNames.CONNECTION; import static io.servicetalk.http.api.HttpHeaderValues.CLOSE; import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.NettyHttpServer.initChannel; import static io.servicetalk.logging.api.LogLevel.TRACE; import static io.servicetalk.transport.netty.internal.NettyIoExecutors.fromNettyEventLoop; @@ -64,8 +64,6 @@ class ServerRespondsOnClosingTest { DefaultHttpHeadersFactory.INSTANCE, DEFAULT_ALLOCATOR, HTTP_1_1); private static final String RESPONSE_PAYLOAD_BODY = "Hello World"; - - private final EmbeddedDuplexChannel channel; private final NettyHttpServerConnection serverConnection; private final Queue requests = new ArrayDeque<>(); @@ -226,7 +224,7 @@ private void handleRequests() { HttpRequest request = exchange.request; HttpResponse response = RESPONSE_FACTORY.ok() .setHeader("Request-Path", request.path()) - .payloadBody(RESPONSE_PAYLOAD_BODY, textSerializer()); + .payloadBody(RESPONSE_PAYLOAD_BODY, textSerializerUtf8()); if (request.hasQueryParameter("serverShouldClose")) { response.setHeader(CONNECTION, CLOSE); } diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentCodingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentCodingTest.java index 709f3a032b..d8feff3700 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentCodingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentCodingTest.java @@ -59,6 +59,8 @@ import static io.servicetalk.http.api.HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE; import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; import static java.util.Arrays.stream; @@ -71,6 +73,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +@Deprecated class ServiceTalkContentCodingTest extends BaseContentCodingTest { private static final BiFunction, StreamingHttpServiceFilterFactory> REQ_FILTER = @@ -215,7 +218,7 @@ protected void assertSuccessful(final ContentCodec encoding) throws Throwable { assertResponse(client().request(client() .get("/") .encoding(encoding) - .payloadBody(payloadAsString((byte) 'a'), textSerializer())).toStreamingResponse()); + .payloadBody(payloadAsString((byte) 'a'), textSerializerUtf8())).toStreamingResponse()); final BlockingStreamingHttpClient blockingStreamingHttpClient = client().asBlockingStreamingClient(); assertResponse(blockingStreamingHttpClient.request(blockingStreamingHttpClient @@ -248,17 +251,17 @@ protected void assertNotSupported(final ContentCodec encoding) throws Exception assertEquals(UNSUPPORTED_MEDIA_TYPE, client().request(client() .get("/") .encoding(encoding) - .payloadBody(payloadAsString((byte) 'a'), textSerializer())).status()); + .payloadBody(payloadAsString((byte) 'a'), textSerializerUtf8())).status()); assertEquals(UNSUPPORTED_MEDIA_TYPE, blockingStreamingHttpClient.request(blockingStreamingHttpClient .get("/") .encoding(encoding) - .payloadBody(singletonList(payloadAsString((byte) 'a')), textSerializer())).status()); + .payloadBody(singletonList(payloadAsString((byte) 'a')), appSerializerUtf8FixLen())).status()); assertEquals(UNSUPPORTED_MEDIA_TYPE, streamingHttpClient.request(streamingHttpClient .get("/") .encoding(encoding) - .payloadBody(from(payloadAsString((byte) 'a')), textSerializer())).toFuture().get().status()); + .payloadBody(from(payloadAsString((byte) 'a')), appSerializerUtf8FixLen())).toFuture().get().status()); } void assertResponseHeaders(final String contentEncodingValue) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentEncodingCompatibilityTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentEncodingCompatibilityTest.java new file mode 100644 index 0000000000..effc4cf6fb --- /dev/null +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentEncodingCompatibilityTest.java @@ -0,0 +1,150 @@ +/* + * Copyright © 2021 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.http.netty; + +import io.servicetalk.concurrent.api.DefaultThreadFactory; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.ContentEncodingHttpRequesterFilter; +import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.HttpResponseStatus; +import io.servicetalk.transport.api.HostAndPort; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpServerCodec; + +import java.net.InetSocketAddress; + +import static io.netty.buffer.Unpooled.wrappedBuffer; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_ENCODING; +import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_2_0; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static io.servicetalk.transport.netty.internal.BuilderUtils.serverChannel; +import static io.servicetalk.transport.netty.internal.NettyIoExecutors.createEventLoopGroup; +import static java.lang.Thread.NORM_PRIORITY; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class ServiceTalkContentEncodingCompatibilityTest extends BaseContentEncodingTest { + @Override + protected void runTest(final HttpProtocol protocol, final Encoder clientEncoding, final Decoders clientDecoder, + final Encoders serverEncoder, final Decoders serverDecoder, final boolean isValid) + throws Throwable { + assumeFalse(protocol.version.equals(HTTP_2_0), "Only testing H1 scenarios yet."); + assumeTrue(isValid, "Only testing successful configurations; Netty doesn't have knowledge " + + "about unsupported compression types."); + + EventLoopGroup serverEventLoopGroup = createEventLoopGroup(2, + new DefaultThreadFactory("server-io", true, NORM_PRIORITY)); + Channel serverAcceptorChannel = null; + try { + ServerBootstrap sb = new ServerBootstrap(); + sb.group(serverEventLoopGroup); + sb.channel(serverChannel(serverEventLoopGroup, InetSocketAddress.class)); + + sb.childHandler(new ChannelInitializer() { + @Override + protected void initChannel(final Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpServerCodec()); + if (!serverDecoder.group.decoders().isEmpty()) { + p.addLast(new HttpContentDecompressor()); + } + if (!serverEncoder.list.isEmpty()) { + p.addLast(new HttpContentCompressor()); + } + p.addLast(EchoServerHandler.INSTANCE); + } + }); + serverAcceptorChannel = sb.bind(localAddress(0)).syncUninterruptibly().channel(); + + try (BlockingHttpClient client = HttpClients.forSingleAddress( + HostAndPort.of((InetSocketAddress) serverAcceptorChannel.localAddress())) + .protocols(protocol.config) + .appendClientFilter(new ContentEncodingHttpRequesterFilter(clientDecoder.group)) + .buildBlocking()) { + HttpResponse response = client.request(client.get("/").contentEncoding(clientEncoding.encoder) + .payloadBody(payloadAsString((byte) 'a'), textSerializerUtf8())); + + assertThat(response.status(), is(HttpResponseStatus.OK)); + + // content encoding should be stripped by the time the decoding is done. + assertThat(response.headers().get(CONTENT_ENCODING), nullValue()); + + assertEquals(payloadAsString((byte) 'b'), response.payloadBody(textSerializerUtf8())); + } + } finally { + if (serverAcceptorChannel != null) { + serverAcceptorChannel.close().syncUninterruptibly(); + } + serverEventLoopGroup.shutdownGracefully(0, 0, MILLISECONDS).syncUninterruptibly(); + } + } + + @ChannelHandler.Sharable + static class EchoServerHandler extends SimpleChannelInboundHandler { + static final EchoServerHandler INSTANCE = new EchoServerHandler(); + + private static final byte[] CONTENT = payload((byte) 'b'); + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { + if (msg instanceof io.netty.handler.codec.http.HttpRequest) { + io.netty.handler.codec.http.HttpRequest req = (io.netty.handler.codec.http.HttpRequest) msg; + FullHttpResponse response = new DefaultFullHttpResponse(req.protocolVersion(), + OK, wrappedBuffer(CONTENT)); + + response.headers() + .set(CONTENT_TYPE, TEXT_PLAIN) + .setInt(CONTENT_LENGTH, response.content().readableBytes()); + + ctx.write(response); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } + } +} diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentEncodingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentEncodingTest.java new file mode 100644 index 0000000000..cd79b89adc --- /dev/null +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkContentEncodingTest.java @@ -0,0 +1,146 @@ +/* + * Copyright © 2020-2021 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.http.netty; + +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.encoding.api.BufferEncoder; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.ContentEncodingHttpRequesterFilter; +import io.servicetalk.http.api.ContentEncodingHttpServiceFilter; +import io.servicetalk.http.api.HttpExecutionStrategy; +import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.HttpServiceContext; +import io.servicetalk.http.api.StreamingHttpClientFilter; +import io.servicetalk.http.api.StreamingHttpRequest; +import io.servicetalk.http.api.StreamingHttpRequester; +import io.servicetalk.http.api.StreamingHttpResponse; +import io.servicetalk.http.api.StreamingHttpResponseFactory; +import io.servicetalk.http.api.StreamingHttpServiceFilter; +import io.servicetalk.transport.api.ServerContext; + +import java.util.function.Supplier; +import javax.annotation.Nullable; + +import static io.servicetalk.buffer.api.CharSequences.contentEqualsIgnoreCase; +import static io.servicetalk.encoding.api.Identity.identityEncoder; +import static io.servicetalk.http.api.HttpHeaderNames.ACCEPT_ENCODING; +import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_ENCODING; +import static io.servicetalk.http.api.HttpResponseStatus.OK; +import static io.servicetalk.http.api.HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; +import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; +import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ServiceTalkContentEncodingTest extends BaseContentEncodingTest { + @Override + protected void runTest( + final HttpProtocol protocol, final Encoder clientEncoding, final Decoders clientDecoder, + final Encoders serverEncoder, final Decoders serverDecoder, boolean valid) throws Throwable { + try (ServerContext serverContext = HttpServers.forAddress(localAddress(0)) + .protocols(protocol.config) + .appendServiceFilter(service -> new StreamingHttpServiceFilter(service) { + @Override + public Single handle(final HttpServiceContext ctx, + final StreamingHttpRequest request, + final StreamingHttpResponseFactory responseFactory) { + return delegate().handle(ctx, request, responseFactory) + .map(resp -> { + // content encoding should be stripped by the time the decoding is done. + assertThat(resp.headers().get(ACCEPT_ENCODING), nullValue()); + + CharSequence contentEncoding = resp.headers().get(CONTENT_ENCODING); + boolean found = contentEncoding == null && (serverEncoder.list.isEmpty() || + !request.headers().contains(ACCEPT_ENCODING)); + for (BufferEncoder be : serverEncoder.list) { + if (contentEncoding == null && contentEqualsIgnoreCase(be.encodingName(), + identityEncoder().encodingName()) || + contentEncoding != null && contentEqualsIgnoreCase(be.encodingName(), + contentEncoding)) { + found = true; + break; + } + } + return found || !valid ? resp : responseFactory.ok().payloadBody(Publisher.from( + "server error: invalid " + CONTENT_ENCODING + ": " + contentEncoding), + appSerializerUtf8FixLen()); + }) + .onErrorReturn(AssertionError.class, cause -> + responseFactory.ok().payloadBody(Publisher.from( + "server error: " + cause.toString()), appSerializerUtf8FixLen())); + } + }) + .appendServiceFilter(new ContentEncodingHttpServiceFilter(serverEncoder.list, serverDecoder.group)) + .listenBlockingAndAwait((ctx, request, responseFactory) -> { + String requestPayload = request.payloadBody(textSerializerUtf8()); + if (payloadAsString((byte) 'a').equals(requestPayload)) { + return responseFactory.ok().payloadBody(payloadAsString((byte) 'b'), textSerializerUtf8()); + } else { + return responseFactory.badRequest().payloadBody(requestPayload, textSerializerUtf8()); + } + }); + BlockingHttpClient client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) + .protocols(protocol.config) + .appendClientFilter(new ContentEncodingHttpRequesterFilter(clientDecoder.group)) + .appendClientFilter(c -> new StreamingHttpClientFilter(c) { + @Override + protected Single request(final StreamingHttpRequester delegate, + final HttpExecutionStrategy strategy, + final StreamingHttpRequest request) { + return Single.defer(() -> { + assertHeader(() -> clientEncoding.encoder == null ? null : + clientEncoding.encoder.encodingName(), + request.headers().get(CONTENT_ENCODING), true); + assertHeader(clientDecoder.group::advertisedMessageEncoding, + request.headers().get(ACCEPT_ENCODING), false); + return delegate.request(strategy, request).subscribeShareContext(); + }); + } + }) + .buildBlocking()) { + HttpResponse response = client.request(client.get("/").contentEncoding(clientEncoding.encoder).payloadBody( + payloadAsString((byte) 'a'), textSerializerUtf8())); + + if (valid) { + assertThat(response.status(), is(OK)); + + // content encoding should be stripped by the time the decoding is done. + assertThat(response.headers().get(CONTENT_ENCODING), nullValue()); + + assertEquals(payloadAsString((byte) 'b'), response.payloadBody(textSerializerUtf8())); + } else { + assertThat(response.status(), is(UNSUPPORTED_MEDIA_TYPE)); + } + } + } + + private static void assertHeader(Supplier expectedSupplier, @Nullable CharSequence actual, + boolean allowNullForIdentity) { + CharSequence expected = expectedSupplier.get(); + if (expected == null) { + assertThat(actual, nullValue()); + } else if (actual != null || !allowNullForIdentity || + !contentEqualsIgnoreCase(identityEncoder().encodingName(), expected)) { + assertThat("expected: '" + expected + "' actual: '" + actual + "'", + contentEqualsIgnoreCase(expected, actual), is(true)); + } + } +} diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkEmptyContentCodingTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkEmptyContentCodingTest.java index edf3005d63..8b734bff96 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkEmptyContentCodingTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkEmptyContentCodingTest.java @@ -21,6 +21,7 @@ import static io.servicetalk.encoding.api.Identity.identity; import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_ENCODING; +@Deprecated class ServiceTalkEmptyContentCodingTest extends ServiceTalkContentCodingTest { @Override diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkToNettyContentCodingCompatibilityTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkToNettyContentCodingCompatibilityTest.java index 158757b1ad..820ef2d241 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkToNettyContentCodingCompatibilityTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/ServiceTalkToNettyContentCodingCompatibilityTest.java @@ -53,6 +53,7 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; +@Deprecated class ServiceTalkToNettyContentCodingCompatibilityTest extends ServiceTalkContentCodingTest { private EventLoopGroup serverEventLoopGroup; diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SslProvidersTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SslProvidersTest.java index c0948b466f..e2a9919601 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SslProvidersTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/SslProvidersTest.java @@ -36,8 +36,7 @@ import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN_UTF_8; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname; import static io.servicetalk.transport.api.SslProvider.JDK; import static io.servicetalk.transport.api.SslProvider.OPENSSL; @@ -66,11 +65,11 @@ private void setUp(SslProvider serverSslProvider, SslProvider clientSslProvider, assertThat(ctx.sslSession(), is(notNullValue())); assertThat(request.path(), is("/path")); assertThat(request.headers().get(CONTENT_TYPE), is(TEXT_PLAIN_UTF_8)); - assertThat(request.payloadBody(textDeserializer()), + assertThat(request.payloadBody(textSerializerUtf8()), is("request-payload-body-" + payloadBody)); return responseFactory.ok() - .payloadBody("response-payload-body-" + payloadBody, textSerializer()); + .payloadBody("response-payload-body-" + payloadBody, textSerializerUtf8()); }); client = HttpClients.forSingleAddress(serverHostAndPort(serverContext)) @@ -111,11 +110,11 @@ void testSecureClientToSecureServer(SslProvider serverSslProvider, setUp(serverSslProvider, clientSslProvider, payloadLength); HttpResponse response = client.request(client.get("/path") - .payloadBody("request-payload-body-" + payloadBody, textSerializer())); + .payloadBody("request-payload-body-" + payloadBody, textSerializerUtf8())); assertThat(response.status(), is(OK)); assertThat(response.headers().get(CONTENT_TYPE), is(TEXT_PLAIN_UTF_8)); - assertThat(response.payloadBody(textDeserializer()), is("response-payload-body-" + payloadBody)); + assertThat(response.payloadBody(textSerializerUtf8()), is("response-payload-body-" + payloadBody)); } private static String randomString(final int length) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/StreamingHttpServiceAsyncContextTest.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/StreamingHttpServiceAsyncContextTest.java index e04f07751c..0b93ce181f 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/StreamingHttpServiceAsyncContextTest.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/StreamingHttpServiceAsyncContextTest.java @@ -32,7 +32,7 @@ import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.http.api.HttpExecutionStrategies.noOffloadsStrategy; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static java.lang.Thread.currentThread; class StreamingHttpServiceAsyncContextTest extends AbstractHttpServiceAsyncContextTest { @@ -92,7 +92,8 @@ private static StreamingHttpService newEmptyAsyncContextService() { AsyncContextMap current = AsyncContext.current(); if (!current.isEmpty()) { - return succeeded(factory.internalServerError().payloadBody(from(current.toString()), textSerializer())); + return succeeded(factory.internalServerError().payloadBody(from(current.toString()), + appSerializerUtf8FixLen())); } CharSequence requestId = request.headers().getAndRemove(REQUEST_ID_HEADER); if (requestId != null) { diff --git a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/Tls13Test.java b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/Tls13Test.java index c78a3e2ce4..263e63c577 100644 --- a/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/Tls13Test.java +++ b/servicetalk-http-netty/src/test/java/io/servicetalk/http/netty/Tls13Test.java @@ -38,8 +38,7 @@ import static io.servicetalk.http.api.HttpHeaderNames.CONTENT_TYPE; import static io.servicetalk.http.api.HttpHeaderValues.TEXT_PLAIN_UTF_8; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.HttpServers.forAddress; import static io.servicetalk.logging.api.LogLevel.TRACE; import static io.servicetalk.test.resources.DefaultTestCerts.serverPemHostname; @@ -95,10 +94,10 @@ void requiredCipher(SslProvider serverSslProvider, SslProvider clientSslProvider .enableWireLogging("servicetalk-tests-wire-logger", TRACE, () -> false) .sslConfig(serverSslBuilder.build()) .listenBlockingAndAwait((ctx, request, responseFactory) -> { - assertThat(request.payloadBody(textDeserializer()), equalTo("request-payload-body")); + assertThat(request.payloadBody(textSerializerUtf8()), equalTo("request-payload-body")); SSLSession sslSession = ctx.sslSession(); assertThat(sslSession, is(notNullValue())); - return responseFactory.ok().payloadBody(sslSession.getProtocol(), textSerializer()); + return responseFactory.ok().payloadBody(sslSession.getProtocol(), textSerializerUtf8()); })) { ClientSslConfigBuilder clientSslBuilder = @@ -121,11 +120,11 @@ void requiredCipher(SslProvider serverSslProvider, SslProvider clientSslProvider assertThat(sslSession.getCipherSuite(), equalTo(cipher)); } HttpResponse response = client.request(client.post("/") - .payloadBody("request-payload-body", textSerializer())); + .payloadBody("request-payload-body", textSerializerUtf8())); assertThat(response.status(), is(OK)); assertThat(response.headers().get(CONTENT_TYPE), is(TEXT_PLAIN_UTF_8)); - assertThat(response.payloadBody(textDeserializer()), equalTo(TLS1_3)); + assertThat(response.payloadBody(textSerializerUtf8()), equalTo(TLS1_3)); } } } diff --git a/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/AsyncContextHttpFilterVerifier.java b/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/AsyncContextHttpFilterVerifier.java index b9753755f2..c86b0d0daa 100644 --- a/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/AsyncContextHttpFilterVerifier.java +++ b/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/AsyncContextHttpFilterVerifier.java @@ -20,9 +20,9 @@ import io.servicetalk.concurrent.api.AsyncContext; import io.servicetalk.concurrent.api.AsyncContextMap; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.http.api.BlockingHttpClient; -import io.servicetalk.http.api.HttpRequest; -import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.BlockingStreamingHttpClient; +import io.servicetalk.http.api.BlockingStreamingHttpRequest; +import io.servicetalk.http.api.BlockingStreamingHttpResponse; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; @@ -33,19 +33,21 @@ import io.servicetalk.http.utils.BeforeFinallyHttpOperator; import io.servicetalk.transport.api.ServerContext; +import java.util.Iterator; +import java.util.List; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.http.netty.HttpServers.forAddress; import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -75,27 +77,31 @@ private AsyncContextHttpFilterVerifier() { public static void verifyServerFilterAsyncContextVisibility(final StreamingHttpServiceFilterFactory filter) throws Exception { final BlockingQueue errors = new LinkedBlockingDeque<>(); - final String payload = "Hello World"; + final List payload = singletonList("Hello World"); final ServerContext serverContext = forAddress(localAddress(0)) .appendServiceFilter(asyncContextAssertionFilter(errors)) .appendServiceFilter(filter) .listenStreamingAndAwait(asyncContextRequestHandler(errors)); - final BlockingHttpClient client = forSingleAddress(serverHostAndPort(serverContext)).buildBlocking(); - final HttpRequest request = client.post("/test") - .payloadBody(payload, textSerializer()); + final BlockingStreamingHttpClient client = forSingleAddress(serverHostAndPort(serverContext)) + .buildBlockingStreaming(); + final BlockingStreamingHttpRequest request = client.post("/test") + .payloadBody(payload, appSerializerUtf8FixLen()); - final HttpResponse resp = client.request(request); + final BlockingStreamingHttpResponse resp = client.request(request); assertThat(resp.status(), is(OK)); - assertThat(resp.payloadBody(textDeserializer()), is(payload)); + Iterator itr = resp.payloadBody(appSerializerUtf8FixLen()).iterator(); + assertThat(itr.hasNext(), is(true)); + assertThat(itr.next(), is(payload.get(0))); + assertThat(itr.hasNext(), is(false)); assertNoAsyncErrors(errors); } private static StreamingHttpService asyncContextRequestHandler(final BlockingQueue errorQueue) { return (ctx, request, respFactory) -> { AsyncContext.put(K1, V1); - return request.payloadBody(textDeserializer()) + return request.payloadBody(appSerializerUtf8FixLen()) .collect(StringBuilder::new, (collector, it) -> { collector.append(it); return collector; @@ -110,7 +116,7 @@ private static StreamingHttpService asyncContextRequestHandler(final BlockingQue assertAsyncContext(K2, V2, errorQueue); assertAsyncContext(K3, V3, errorQueue); return body; - }), textSerializer()).transformPayloadBody(publisher -> + }), appSerializerUtf8FixLen()).transformPayloadBody(publisher -> publisher.beforeSubscriber(() -> new PublisherSource.Subscriber() { @Override public void onSubscribe(final PublisherSource.Subscription subscription) { diff --git a/servicetalk-http-router-jersey/src/main/java/io/servicetalk/http/router/jersey/DefaultJerseyStreamingHttpRouter.java b/servicetalk-http-router-jersey/src/main/java/io/servicetalk/http/router/jersey/DefaultJerseyStreamingHttpRouter.java index 89ea075003..48aaa2110f 100644 --- a/servicetalk-http-router-jersey/src/main/java/io/servicetalk/http/router/jersey/DefaultJerseyStreamingHttpRouter.java +++ b/servicetalk-http-router-jersey/src/main/java/io/servicetalk/http/router/jersey/DefaultJerseyStreamingHttpRouter.java @@ -224,8 +224,8 @@ private void handle0(final HttpServiceContext serviceCtx, final StreamingHttpReq } catch (IllegalArgumentException cause) { Buffer message = serviceCtx.executionContext().bufferAllocator().fromAscii(cause.getMessage()); StreamingHttpResponse response = factory.badRequest().payloadBody(from(message)); - response.headers().add(CONTENT_LENGTH, Integer.toString(message.readableBytes())); - response.headers().add(CONTENT_TYPE, TEXT_PLAIN); + response.headers().set(CONTENT_LENGTH, Integer.toString(message.readableBytes())); + response.headers().set(CONTENT_TYPE, TEXT_PLAIN); subscriber.onSuccess(response); return; } diff --git a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/AsynchronousResources.java b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/AsynchronousResources.java index 3445d7df49..0d234e0533 100644 --- a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/AsynchronousResources.java +++ b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/AsynchronousResources.java @@ -22,12 +22,8 @@ import io.servicetalk.concurrent.api.Executor; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.http.router.jersey.AbstractResourceTest.TestFiltered; import io.servicetalk.http.router.jersey.TestPojo; -import io.servicetalk.serialization.api.DefaultSerializer; -import io.servicetalk.serialization.api.Serializer; -import io.servicetalk.serialization.api.TypeHolder; import io.servicetalk.transport.api.ConnectionContext; import org.glassfish.jersey.internal.util.collection.Ref; @@ -71,6 +67,7 @@ import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; import static io.servicetalk.http.router.jersey.resources.AsynchronousResources.PATH; +import static io.servicetalk.http.router.jersey.resources.SerializerUtils.MAP_STRING_OBJECT_SERIALIZER; import static java.lang.System.arraycopy; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.util.Collections.singletonMap; @@ -96,11 +93,6 @@ public class AsynchronousResources { public static final String PATH = "/async"; - private static final Serializer SERIALIZER = new DefaultSerializer(new JacksonSerializationProvider()); - private static final TypeHolder> STRING_OBJECT_MAP_TYPE = - new TypeHolder>() { - }; - @Context private ConnectionContext ctx; @@ -130,9 +122,9 @@ public Single postJsonBufSingleInSingleOut(@QueryParam("fail") final boo return fail ? defer(() -> failed(DELIBERATE_EXCEPTION)) : requestContent.map(buf -> { final Map responseContent = - new HashMap<>(SERIALIZER.deserializeAggregatedSingle(buf, STRING_OBJECT_MAP_TYPE)); + new HashMap<>(MAP_STRING_OBJECT_SERIALIZER.deserialize(buf, allocator)); responseContent.put("foo", "bar6"); - return SERIALIZER.serialize(responseContent, allocator); + return MAP_STRING_OBJECT_SERIALIZER.serialize(responseContent, allocator); }); } diff --git a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/ExecutionStrategyResources.java b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/ExecutionStrategyResources.java index e7f2ffd349..bf30fed688 100644 --- a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/ExecutionStrategyResources.java +++ b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/ExecutionStrategyResources.java @@ -19,12 +19,9 @@ 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.http.api.StreamingHttpRequest; import io.servicetalk.router.api.NoOffloadsRouteExecutionStrategy; import io.servicetalk.router.api.RouteExecutionStrategy; -import io.servicetalk.serialization.api.DefaultSerializer; -import io.servicetalk.serialization.api.Serializer; import io.servicetalk.transport.api.ConnectionContext; import org.glassfish.jersey.server.ManagedAsync; @@ -47,6 +44,7 @@ import static io.servicetalk.concurrent.api.Single.defer; import static io.servicetalk.concurrent.api.Single.succeeded; +import static io.servicetalk.http.router.jersey.resources.SerializerUtils.MAP_STRING_STRING_SERIALIZER; import static java.lang.Thread.currentThread; import static java.util.concurrent.CompletableFuture.completedFuture; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; @@ -64,8 +62,6 @@ private ExecutionStrategyResources() { @Produces(APPLICATION_JSON) public abstract static class AbstractExecutionStrategyResource { - private static final Serializer SERIALIZER = new DefaultSerializer(new JacksonSerializationProvider()); - @Context private ConnectionContext ctx; @@ -238,7 +234,7 @@ private Single getThreadingInfoSingleBuffer() { final Map threadingInfo = getThreadingInfo(ctx, req, uriInfo); return defer(() -> { threadingInfo.put(RS_THREAD_NAME, currentThread().getName()); - return succeeded(SERIALIZER.serialize(threadingInfo, allocator)); + return succeeded(MAP_STRING_STRING_SERIALIZER.serialize(threadingInfo, allocator)); }); } diff --git a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SerializerUtils.java b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SerializerUtils.java new file mode 100644 index 0000000000..bf918611fc --- /dev/null +++ b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SerializerUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2021 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.http.router.jersey.resources; + +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.Map; + +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; + +final class SerializerUtils { + private static final TypeReference> STRING_OBJECT_MAP_TYPE = + new TypeReference>() { }; + private static final TypeReference> MAP_STRING_STRING_TYPE = + new TypeReference>() { }; + static final SerializerDeserializer> MAP_STRING_OBJECT_SERIALIZER = + JACKSON.serializerDeserializer(STRING_OBJECT_MAP_TYPE); + static final StreamingSerializerDeserializer> MAP_STRING_OBJECT_STREAM_SERIALIZER = + JACKSON.streamingSerializerDeserializer(STRING_OBJECT_MAP_TYPE); + static final SerializerDeserializer> MAP_STRING_STRING_SERIALIZER = + JACKSON.serializerDeserializer(MAP_STRING_STRING_TYPE); + + private SerializerUtils() { + } +} diff --git a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SynchronousResources.java b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SynchronousResources.java index 38026b2b49..62bacf602e 100644 --- a/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SynchronousResources.java +++ b/servicetalk-http-router-jersey/src/testFixtures/java/io/servicetalk/http/router/jersey/resources/SynchronousResources.java @@ -20,13 +20,9 @@ import io.servicetalk.buffer.api.CompositeBuffer; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.router.jersey.AbstractResourceTest.TestFiltered; import io.servicetalk.http.router.jersey.TestPojo; -import io.servicetalk.serialization.api.DefaultSerializer; -import io.servicetalk.serialization.api.Serializer; -import io.servicetalk.serialization.api.TypeHolder; import io.servicetalk.transport.api.ConnectionContext; import java.io.InputStream; @@ -60,6 +56,8 @@ import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; import static io.servicetalk.http.router.jersey.TestUtils.getContentAsString; +import static io.servicetalk.http.router.jersey.resources.SerializerUtils.MAP_STRING_OBJECT_SERIALIZER; +import static io.servicetalk.http.router.jersey.resources.SerializerUtils.MAP_STRING_OBJECT_STREAM_SERIALIZER; import static io.servicetalk.http.router.jersey.resources.SynchronousResources.PATH; import static java.lang.System.arraycopy; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -85,11 +83,6 @@ public class SynchronousResources { public static final String PATH = "/sync"; - private static final Serializer SERIALIZER = new DefaultSerializer(new JacksonSerializationProvider()); - private static final TypeHolder> STRING_OBJECT_MAP_TYPE = - new TypeHolder>() { - }; - @Context private ConnectionContext ctx; @@ -366,8 +359,8 @@ public Publisher postJsonMapInPubOut(final Map requestCo // and ServiceTalk streaming serialization is used for the response final Map responseContent = new HashMap<>(requestContent); responseContent.put("foo", "bar3"); - return SERIALIZER.serialize(from(responseContent), ctx.executionContext().bufferAllocator(), - STRING_OBJECT_MAP_TYPE); + return MAP_STRING_OBJECT_STREAM_SERIALIZER.serialize(from(responseContent), + ctx.executionContext().bufferAllocator()); } @Consumes(APPLICATION_JSON) @@ -378,7 +371,8 @@ public Map postJsonPubInMapOut(final Publisher requestCo // ServiceTalk streaming deserialization is used for the request // and Jersey's JacksonJsonProvider (thus blocking IO) is used for response serialization final Map requestData = - SERIALIZER.deserialize(requestContent, STRING_OBJECT_MAP_TYPE).toIterable().iterator().next(); + MAP_STRING_OBJECT_STREAM_SERIALIZER.deserialize(requestContent, + ctx.executionContext().bufferAllocator()).toIterable().iterator().next(); final Map responseContent = new HashMap<>(requestData); responseContent.put("foo", "bar4"); return responseContent; @@ -390,15 +384,15 @@ public Map postJsonPubInMapOut(final Publisher requestCo @POST public Publisher postJsonPubInPubOut(final Publisher requestContent) { // ServiceTalk streaming is used for both request deserialization and response serialization - final Publisher> response = - SERIALIZER.deserialize(requestContent, STRING_OBJECT_MAP_TYPE) + final Publisher> response = MAP_STRING_OBJECT_STREAM_SERIALIZER.deserialize( + requestContent, ctx.executionContext().bufferAllocator()) .map(requestData -> { final Map responseContent = new HashMap<>(requestData); responseContent.put("foo", "bar5"); return responseContent; }); - return SERIALIZER.serialize(response, ctx.executionContext().bufferAllocator(), STRING_OBJECT_MAP_TYPE); + return MAP_STRING_OBJECT_STREAM_SERIALIZER.serialize(response, ctx.executionContext().bufferAllocator()); } @Consumes(APPLICATION_JSON) @@ -409,9 +403,9 @@ public Response postJsonBufSingleInSingleOutResponse(final Single reques final BufferAllocator allocator = ctx.executionContext().bufferAllocator(); final Single response = requestContent.map(buf -> { final Map responseContent = - new HashMap<>(SERIALIZER.deserializeAggregatedSingle(buf, STRING_OBJECT_MAP_TYPE)); + new HashMap<>(MAP_STRING_OBJECT_SERIALIZER.deserialize(buf, allocator)); responseContent.put("foo", "bar6"); - return SERIALIZER.serialize(responseContent, allocator); + return MAP_STRING_OBJECT_SERIALIZER.serialize(responseContent, allocator); }); return accepted(new GenericEntity>(response) { }).build(); diff --git a/servicetalk-http-router-predicate/src/test/java/io/servicetalk/http/router/predicate/HttpServerOverrideOffloadingTest.java b/servicetalk-http-router-predicate/src/test/java/io/servicetalk/http/router/predicate/HttpServerOverrideOffloadingTest.java index 6b3488821c..42b3a4124b 100644 --- a/servicetalk-http-router-predicate/src/test/java/io/servicetalk/http/router/predicate/HttpServerOverrideOffloadingTest.java +++ b/servicetalk-http-router-predicate/src/test/java/io/servicetalk/http/router/predicate/HttpServerOverrideOffloadingTest.java @@ -46,7 +46,7 @@ import static io.servicetalk.concurrent.api.SourceAdapters.toSource; import static io.servicetalk.http.api.HttpExecutionStrategies.defaultStrategy; import static io.servicetalk.http.api.HttpExecutionStrategies.noOffloadsStrategy; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.appSerializerUtf8FixLen; import static io.servicetalk.test.resources.TestUtils.assertNoAsyncErrors; import static io.servicetalk.transport.netty.NettyIoExecutors.createIoExecutor; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; @@ -133,7 +133,7 @@ public Single handle(final HttpServiceContext ctx, final "Thread: " + currentThread())); } }).ignoreElements()).subscribe(cp); - return succeeded(responseFactory.ok().payloadBody(from("Hello"), textSerializer()) + return succeeded(responseFactory.ok().payloadBody(from("Hello"), appSerializerUtf8FixLen()) .transformPayloadBody(p -> p.beforeRequest(__ -> { if (isInvalidThread.test(currentThread())) { errors.add(new AssertionError("Invalid thread calling response payload " + diff --git a/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TestUtils.java b/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TestUtils.java index 2071daea30..c0a7d1efec 100644 --- a/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TestUtils.java +++ b/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TestUtils.java @@ -15,6 +15,7 @@ */ package io.servicetalk.opentracing.http; +import io.servicetalk.http.api.HttpSerializerDeserializer; import io.servicetalk.opentracing.inmemory.api.InMemorySpan; import io.servicetalk.opentracing.inmemory.api.InMemorySpanEventListener; import io.servicetalk.opentracing.internal.TracingConstants; @@ -28,6 +29,8 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; +import static io.servicetalk.data.jackson.JacksonSerializerFactory.JACKSON; +import static io.servicetalk.http.api.HttpSerializers.jsonSerializer; import static io.servicetalk.log4j2.mdc.utils.LoggerStringWriter.assertContainsMdcPair; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -39,6 +42,8 @@ final class TestUtils { "filter response onSubscribe path={}", "filter response onNext path={}", "filter response terminated path={}"}; + static final HttpSerializerDeserializer SPAN_STATE_SERIALIZER = + jsonSerializer(JACKSON.serializerDeserializer(TestSpanState.class)); private TestUtils() { } // no instantiation diff --git a/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpRequesterFilterTest.java b/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpRequesterFilterTest.java index 49a3aabcd6..62749d9cd6 100644 --- a/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpRequesterFilterTest.java +++ b/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpRequesterFilterTest.java @@ -17,13 +17,11 @@ import io.servicetalk.concurrent.PublisherSource; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.http.api.FilterableStreamingHttpClient; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpExecutionStrategy; import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.StreamingHttpClientFilter; import io.servicetalk.http.api.StreamingHttpClientFilterFactory; import io.servicetalk.http.api.StreamingHttpRequest; @@ -56,15 +54,14 @@ import static io.opentracing.tag.Tags.HTTP_URL; import static io.opentracing.tag.Tags.SPAN_KIND; import static io.opentracing.tag.Tags.SPAN_KIND_CLIENT; -import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; import static io.servicetalk.http.api.HttpRequestMethod.GET; import static io.servicetalk.http.api.HttpResponseStatus.OK; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.log4j2.mdc.utils.LoggerStringWriter.stableAccumulated; import static io.servicetalk.opentracing.asynccontext.AsyncContextInMemoryScopeManager.SCOPE_MANAGER; +import static io.servicetalk.opentracing.http.TestUtils.SPAN_STATE_SERIALIZER; import static io.servicetalk.opentracing.http.TestUtils.TRACING_TEST_LOG_LINE_PREFIX; import static io.servicetalk.opentracing.http.TestUtils.isHexId; import static io.servicetalk.opentracing.http.TestUtils.verifyTraceIdPresentInLogs; @@ -90,7 +87,6 @@ @ExtendWith(MockitoExtension.class) class TracingHttpRequesterFilterTest { private static final Logger LOGGER = LoggerFactory.getLogger(TracingHttpRequesterFilterTest.class); - private static final HttpSerializationProvider httpSerializer = jsonSerializer(new JacksonSerializationProvider()); @Mock private Tracer mockTracer; @@ -117,8 +113,7 @@ void testInjectWithNoParent() throws Exception { .appendClientFilter(new TracingHttpRequesterFilter(tracer, "testClient")) .appendClientFilter(new TestTracingLoggerFilter(TRACING_TEST_LOG_LINE_PREFIX)).build()) { HttpResponse response = client.request(client.get(requestUrl)).toFuture().get(); - TestSpanState serverSpanState = response.payloadBody(httpSerializer.deserializerFor( - TestSpanState.class)); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); assertThat(serverSpanState.traceId, isHexId()); assertThat(serverSpanState.spanId, isHexId()); @@ -155,8 +150,7 @@ void testInjectWithParent() throws Exception { InMemorySpan clientSpan = tracer.buildSpan("test").start(); try (Scope ignored = tracer.activateSpan(clientSpan)) { HttpResponse response = client.request(client.get(requestUrl)).toFuture().get(); - TestSpanState serverSpanState = response.payloadBody(httpSerializer.deserializerFor( - TestSpanState.class)); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); assertThat(serverSpanState.traceId, isHexId()); assertThat(serverSpanState.spanId, isHexId()); @@ -205,14 +199,14 @@ void tracerThrowsReturnsErrorResponse() throws Exception { private static ServerContext buildServer() throws Exception { return HttpServers.forAddress(localAddress(0)) - .listenStreamingAndAwait((ctx, request, responseFactory) -> - succeeded(responseFactory.ok().payloadBody(from(new TestSpanState( + .listenAndAwait((ctx, request, responseFactory) -> + succeeded(responseFactory.ok().payloadBody(new TestSpanState( valueOf(request.headers().get(TRACE_ID)), valueOf(request.headers().get(SPAN_ID)), toStringOrNull(request.headers().get(PARENT_SPAN_ID)), "1".equals(valueOf(request.headers().get(SAMPLED))), false - )), httpSerializer.serializerFor(TestSpanState.class)))); + ), SPAN_STATE_SERIALIZER))); } @Nullable diff --git a/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpServiceFilterTest.java b/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpServiceFilterTest.java index 8b1acd0075..b1e03eb8f1 100644 --- a/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpServiceFilterTest.java +++ b/servicetalk-opentracing-http/src/test/java/io/servicetalk/opentracing/http/TracingHttpServiceFilterTest.java @@ -17,11 +17,9 @@ import io.servicetalk.concurrent.PublisherSource; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.data.jackson.JacksonSerializationProvider; import io.servicetalk.http.api.HttpClient; import io.servicetalk.http.api.HttpRequest; import io.servicetalk.http.api.HttpResponse; -import io.servicetalk.http.api.HttpSerializationProvider; import io.servicetalk.http.api.HttpServiceContext; import io.servicetalk.http.api.StreamingHttpRequest; import io.servicetalk.http.api.StreamingHttpResponse; @@ -52,16 +50,15 @@ import javax.annotation.Nullable; import static io.opentracing.tag.Tags.ERROR; -import static io.servicetalk.concurrent.api.Publisher.from; import static io.servicetalk.concurrent.api.Single.succeeded; import static io.servicetalk.concurrent.internal.DeliberateException.DELIBERATE_EXCEPTION; import static io.servicetalk.http.api.HttpResponseStatus.INTERNAL_SERVER_ERROR; -import static io.servicetalk.http.api.HttpSerializationProviders.jsonSerializer; -import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; import static io.servicetalk.http.netty.AsyncContextHttpFilterVerifier.verifyServerFilterAsyncContextVisibility; import static io.servicetalk.http.netty.HttpClients.forSingleAddress; import static io.servicetalk.log4j2.mdc.utils.LoggerStringWriter.stableAccumulated; import static io.servicetalk.opentracing.asynccontext.AsyncContextInMemoryScopeManager.SCOPE_MANAGER; +import static io.servicetalk.opentracing.http.TestUtils.SPAN_STATE_SERIALIZER; import static io.servicetalk.opentracing.http.TestUtils.TRACING_TEST_LOG_LINE_PREFIX; import static io.servicetalk.opentracing.http.TestUtils.randomHexId; import static io.servicetalk.opentracing.http.TestUtils.verifyTraceIdPresentInLogs; @@ -71,6 +68,7 @@ import static io.servicetalk.opentracing.internal.ZipkinHeaderNames.TRACE_ID; import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress; import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort; +import static java.lang.Boolean.TRUE; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; @@ -86,7 +84,6 @@ @ExtendWith(MockitoExtension.class) class TracingHttpServiceFilterTest { private static final Logger LOGGER = LoggerFactory.getLogger(TracingHttpServiceFilterTest.class); - private static final HttpSerializationProvider httpSerializer = jsonSerializer(new JacksonSerializationProvider()); @Mock private Tracer mockTracer; @@ -114,19 +111,19 @@ private static ServerContext buildServer(CountingInMemorySpanEventListener spanL return HttpServers.forAddress(localAddress(0)) .appendServiceFilter(new TracingHttpServiceFilter(tracer, "testServer")) .appendServiceFilter(new TestTracingLoggerFilter(TRACING_TEST_LOG_LINE_PREFIX)) - .listenStreamingAndAwait((ctx, request, responseFactory) -> { + .listenAndAwait((ctx, request, responseFactory) -> { InMemorySpan span = tracer.activeSpan(); if (span == null) { - return succeeded(responseFactory.internalServerError().payloadBody(from("span not found"), - textSerializer())); + return succeeded(responseFactory.internalServerError().payloadBody("span not found", + textSerializerUtf8())); } - return succeeded(responseFactory.ok().payloadBody(from(new TestSpanState( + return succeeded(responseFactory.ok().payloadBody(new TestSpanState( span.context().toTraceId(), span.context().toSpanId(), span.context().parentSpanId(), - span.context().isSampled(), - span.tags().containsKey(ERROR.getKey()))), - httpSerializer.serializerFor(TestSpanState.class))); + TRUE.equals(span.context().isSampled()), + span.tags().containsKey(ERROR.getKey())), + SPAN_STATE_SERIALIZER)); }); } @@ -146,8 +143,7 @@ void testRequestWithTraceKey() throws Exception { .set(PARENT_SPAN_ID, parentSpanId) .set(SAMPLED, "0"); HttpResponse response = client.request(request).toFuture().get(); - TestSpanState serverSpanState = response.payloadBody(httpSerializer.deserializerFor( - TestSpanState.class)); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); assertSpan(spanListener, traceId, spanId, requestUrl, serverSpanState, false); } } @@ -165,8 +161,7 @@ void testRequestWithTraceKeyWithoutSampled() throws Exception { request.headers().set(TRACE_ID, traceId) .set(SPAN_ID, spanId); HttpResponse response = client.request(request).toFuture().get(); - TestSpanState serverSpanState = response.payloadBody(httpSerializer.deserializerFor( - TestSpanState.class)); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); assertSpan(spanListener, traceId, spanId, requestUrl, serverSpanState, true); } } @@ -185,8 +180,7 @@ void testRequestWithTraceKeyWithNegativeSampledAndAlwaysTrueSampler() throws Exc .set(SPAN_ID, spanId) .set(SAMPLED, "0"); HttpResponse response = client.request(request).toFuture().get(); - TestSpanState serverSpanState = response.payloadBody(httpSerializer.deserializerFor( - TestSpanState.class)); + TestSpanState serverSpanState = response.payloadBody(SPAN_STATE_SERIALIZER); assertSpan(spanListener, traceId, spanId, requestUrl, serverSpanState, true); } } diff --git a/servicetalk-opentracing-zipkin-publisher/src/main/java/io/servicetalk/opentracing/zipkin/publisher/reporter/HttpReporter.java b/servicetalk-opentracing-zipkin-publisher/src/main/java/io/servicetalk/opentracing/zipkin/publisher/reporter/HttpReporter.java index c7c2c3d21d..1d72a8f63f 100644 --- a/servicetalk-opentracing-zipkin-publisher/src/main/java/io/servicetalk/opentracing/zipkin/publisher/reporter/HttpReporter.java +++ b/servicetalk-opentracing-zipkin-publisher/src/main/java/io/servicetalk/opentracing/zipkin/publisher/reporter/HttpReporter.java @@ -178,7 +178,7 @@ private static Function encodedSpansReporter(final HttpClie throw new IllegalArgumentException("Unknown codec: " + codec); } return encodedSpans -> client.request( - client.post(path).addHeader(CONTENT_TYPE, contentType).payloadBody(encodedSpans)) + client.post(path).setHeader(CONTENT_TYPE, contentType).payloadBody(encodedSpans)) .beforeOnSuccess(response -> { HttpResponseStatus status = response.status(); if (status.statusClass() != SUCCESSFUL_2XX) { diff --git a/servicetalk-serialization-api/build.gradle b/servicetalk-serialization-api/build.gradle index 31b84f181a..dc7f26e8a5 100644 --- a/servicetalk-serialization-api/build.gradle +++ b/servicetalk-serialization-api/build.gradle @@ -19,6 +19,7 @@ apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" dependencies { api project(":servicetalk-buffer-api") api project(":servicetalk-concurrent-api") + api project(":servicetalk-serializer-api") implementation project(":servicetalk-annotations") implementation project(":servicetalk-concurrent-api-internal") diff --git a/servicetalk-serialization-api/gradle/spotbugs/main-exclusions.xml b/servicetalk-serialization-api/gradle/spotbugs/main-exclusions.xml new file mode 100644 index 0000000000..a664a9d333 --- /dev/null +++ b/servicetalk-serialization-api/gradle/spotbugs/main-exclusions.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/DefaultSerializer.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/DefaultSerializer.java index 549d152132..f19a1955e2 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/DefaultSerializer.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/DefaultSerializer.java @@ -44,7 +44,15 @@ /** * Default implementation for {@link Serializer}. + * @deprecated Use implementations of following types: + *
      + *
    • {@link io.servicetalk.serializer.api.Serializer}
    • + *
    • {@link io.servicetalk.serializer.api.StreamingSerializer}
    • + *
    • {@link io.servicetalk.serializer.api.Deserializer}
    • + *
    • {@link io.servicetalk.serializer.api.StreamingDeserializer}
    • + *
    */ +@Deprecated public final class DefaultSerializer implements Serializer { /** diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationException.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationException.java index 6a08b3722b..d7e64f49d3 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationException.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationException.java @@ -17,9 +17,10 @@ /** * Exception indicating an error during serialization or deserialization. + * @deprecated Use {@link io.servicetalk.serializer.api.SerializationException}. */ -public final class SerializationException extends RuntimeException { - +@Deprecated +public final class SerializationException extends io.servicetalk.serializer.api.SerializationException { private static final long serialVersionUID = 4181881136732849119L; /** diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationProvider.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationProvider.java index 1fffa456fa..b3e68a27d8 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationProvider.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/SerializationProvider.java @@ -18,10 +18,14 @@ import io.servicetalk.buffer.api.Buffer; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; /** * A provider of serialization implementation for {@link Serializer}. + * @deprecated General {@link Type} serialization is not supported by all serializers. Defer + * to your specific {@link io.servicetalk.serializer.api.Serializer} implementation. */ +@Deprecated public interface SerializationProvider { /** diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/Serializer.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/Serializer.java index b55260af10..99af335f0f 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/Serializer.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/Serializer.java @@ -29,13 +29,21 @@ /** * A contract for serialization and deserialization. + * @deprecated Use the following types: + *
      + *
    • {@link io.servicetalk.serializer.api.Serializer}
    • + *
    • {@link io.servicetalk.serializer.api.StreamingSerializer}
    • + *
    • {@link io.servicetalk.serializer.api.Deserializer}
    • + *
    • {@link io.servicetalk.serializer.api.StreamingDeserializer}
    • + *
    */ +@Deprecated public interface Serializer { /** * Transforms the passed {@link Publisher} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Publisher, BufferAllocator)}. * @param source {@link Publisher} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param type The class for {@link T}, the object to be serialized. @@ -44,12 +52,13 @@ public interface Serializer { * @return A transformed {@link Publisher} such that each contained element in the original {@link Publisher} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Publisher serialize(Publisher source, BufferAllocator allocator, Class type); /** * Transforms the passed {@link Iterable} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link Iterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param type The class for {@link T}, the object to be serialized. @@ -58,12 +67,13 @@ public interface Serializer { * @return A transformed {@link Iterable} such that each contained element in the original {@link Iterable} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Iterable serialize(Iterable source, BufferAllocator allocator, Class type); /** * Transforms the passed {@link BlockingIterable} such that each contained element of type {@link T} is serialized * into a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link BlockingIterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param type The class for {@link T}, the object to be serialized. @@ -72,12 +82,13 @@ public interface Serializer { * @return A transformed {@link BlockingIterable} such that each contained element in the original * {@link BlockingIterable} is transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated BlockingIterable serialize(BlockingIterable source, BufferAllocator allocator, Class type); /** * Transforms the passed {@link Publisher} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Publisher, BufferAllocator)}. * @param source {@link Publisher} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param type The class for {@link T}, the object to be serialized. @@ -88,13 +99,14 @@ public interface Serializer { * @return A transformed {@link Publisher} such that each contained element in the original {@link Publisher} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Publisher serialize(Publisher source, BufferAllocator allocator, Class type, IntUnaryOperator bytesEstimator); /** * Transforms the passed {@link Iterable} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link Iterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param type The class for {@link T}, the object to be serialized. @@ -105,13 +117,14 @@ Publisher serialize(Publisher source, BufferAllocator allocator, * @return A transformed {@link Iterable} such that each contained element in the original {@link Iterable} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Iterable serialize(Iterable source, BufferAllocator allocator, Class type, IntUnaryOperator bytesEstimator); /** * Transforms the passed {@link BlockingIterable} such that each contained element of type {@link T} is serialized * into a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link BlockingIterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param type The class for {@link T}, the object to be serialized. @@ -122,13 +135,14 @@ Iterable serialize(Iterable source, BufferAllocator allocator, Cl * @return A transformed {@link BlockingIterable} such that each contained element in the original * {@link BlockingIterable} is transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated BlockingIterable serialize(BlockingIterable source, BufferAllocator allocator, Class type, IntUnaryOperator bytesEstimator); /** * Transforms the passed {@link Publisher} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Publisher, BufferAllocator)}. * @param source {@link Publisher} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be serialized. @@ -137,12 +151,13 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link Publisher} such that each contained element in the original {@link Publisher} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Publisher serialize(Publisher source, BufferAllocator allocator, TypeHolder typeHolder); /** * Transforms the passed {@link Iterable} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link Iterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be serialized. @@ -151,12 +166,13 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link Iterable} such that each contained element in the original {@link Iterable} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Iterable serialize(Iterable source, BufferAllocator allocator, TypeHolder typeHolder); /** * Transforms the passed {@link BlockingIterable} such that each contained element of type {@link T} is serialized * into a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link BlockingIterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be serialized. @@ -165,13 +181,14 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link BlockingIterable} such that each contained element in the original * {@link BlockingIterable} is transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated BlockingIterable serialize(BlockingIterable source, BufferAllocator allocator, TypeHolder typeHolder); /** * Transforms the passed {@link Publisher} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Publisher, BufferAllocator)}. * @param source {@link Publisher} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be serialized. @@ -182,13 +199,14 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link Publisher} such that each contained element in the original {@link Publisher} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Publisher serialize(Publisher source, BufferAllocator allocator, TypeHolder typeHolder, IntUnaryOperator bytesEstimator); /** * Transforms the passed {@link Iterable} such that each contained element of type {@link T} is serialized into * a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link Iterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be serialized. @@ -199,13 +217,14 @@ Publisher serialize(Publisher source, BufferAllocator allocator, * @return A transformed {@link Iterable} such that each contained element in the original {@link Iterable} is * transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated Iterable serialize(Iterable source, BufferAllocator allocator, TypeHolder typeHolder, IntUnaryOperator bytesEstimator); /** * Transforms the passed {@link BlockingIterable} such that each contained element of type {@link T} is serialized * into a {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer#serialize(Iterable, BufferAllocator)}. * @param source {@link BlockingIterable} containing objects to serialize. * @param allocator The {@link BufferAllocator} used to allocate {@link Buffer}s. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be serialized. @@ -216,23 +235,25 @@ Iterable serialize(Iterable source, BufferAllocator allocator, Ty * @return A transformed {@link BlockingIterable} such that each contained element in the original * {@link BlockingIterable} is transformed from type {@link T} to a {@link Buffer}. */ + @Deprecated BlockingIterable serialize(BlockingIterable source, BufferAllocator allocator, TypeHolder typeHolder, IntUnaryOperator bytesEstimator); /** * Serializes the passed object {@code toSerialize} to the returned {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.Serializer#serialize(Object, BufferAllocator)}. * @param toSerialize Object to serialize. * @param allocator {@link BufferAllocator} to allocate the returned {@link Buffer}. * @param The data type to serialize. * * @return {@link Buffer} containing the serialized representation of {@code toSerialize}. */ + @Deprecated Buffer serialize(T toSerialize, BufferAllocator allocator); /** * Serializes the passed object {@code toSerialize} to the returned {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.Serializer#serialize(Object, BufferAllocator)}. * @param toSerialize Object to serialize. * @param allocator {@link BufferAllocator} to allocate the returned {@link Buffer}. * @param bytesEstimate An estimate for the size in bytes of the serialized representation of {@code toSerialize}. @@ -240,15 +261,17 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * * @return {@link Buffer} containing the serialized representation of {@code toSerialize}. */ + @Deprecated Buffer serialize(T toSerialize, BufferAllocator allocator, int bytesEstimate); /** * Serializes the passed object {@code toSerialize} to the passed {@link Buffer}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.Serializer} * @param toSerialize Object to serialize. * @param destination The {@link Buffer} to which the serialized representation of {@code toSerialize} is written. * @param The data type to serialize. */ + @Deprecated void serialize(T toSerialize, Buffer destination); /** @@ -259,7 +282,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat *

    * If all content has been aggregated into a single {@link Buffer}, {@link #deserializeAggregated(Buffer, Class)} * can be used. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Publisher, BufferAllocator)}. * @param source {@link Publisher} containing {@link Buffer}s to deserialize. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be deserialized. * @param The data type to deserialize. @@ -267,6 +291,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link Publisher} such that each contained element in the original {@link Publisher} is * transformed from type {@link Buffer} to an instance of {@link T}. */ + @Deprecated Publisher deserialize(Publisher source, TypeHolder typeHolder); /** @@ -278,7 +303,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat *

    * If all content has been aggregated into a single {@link Buffer}, * {@link #deserializeAggregated(Buffer, TypeHolder)} can be used. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Iterable, BufferAllocator)}. * @param source {@link Iterable} containing {@link Buffer}s to deserialize. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be deserialized. * @param The data type to deserialize. @@ -286,6 +312,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link CloseableIterable} such that each contained element in the original {@link Iterable} * is transformed from type {@link Buffer} to an instance of {@link T}. */ + @Deprecated CloseableIterable deserialize(Iterable source, TypeHolder typeHolder); /** @@ -297,7 +324,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat *

    * If all content has been aggregated into a single {@link Buffer}, {@link #deserializeAggregated(Buffer, Class)} * can be used. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Iterable, BufferAllocator)}. * @param source {@link BlockingIterable} containing {@link Buffer}s to deserialize. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be deserialized. * @param The data type to deserialize. @@ -305,6 +333,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link BlockingIterable} such that each contained element in the original * {@link BlockingIterable} is transformed from type {@link Buffer} to an instance of {@link T}. */ + @Deprecated BlockingIterable deserialize(BlockingIterable source, TypeHolder typeHolder); /** @@ -315,7 +344,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat *

    * If all content has been aggregated into a single {@link Buffer}, {@link #deserializeAggregated(Buffer, Class)} * can be used. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Publisher, BufferAllocator)}. * @param source {@link Publisher} containing {@link Buffer}s to deserialize. * @param type The class for {@link T}, the object to be deserialized. * @param The data type to deserialize. @@ -323,6 +353,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link Publisher} such that each contained element in the original {@link Publisher} is * transformed from type {@link Buffer} to an instance of {@link T}. */ + @Deprecated Publisher deserialize(Publisher source, Class type); /** @@ -334,7 +365,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat *

    * If all content has been aggregated into a single {@link Buffer}, {@link #deserializeAggregated(Buffer, Class)} * can be used. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Iterable, BufferAllocator)}. * @param source {@link Iterable} containing {@link Buffer}s to deserialize. * @param type The class for {@link T}, the object to be deserialized. * @param The data type to deserialize. @@ -342,6 +374,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link CloseableIterable} such that each contained element in the original {@link Iterable} * is transformed from type {@link Buffer} to an instance of {@link T}. */ + @Deprecated CloseableIterable deserialize(Iterable source, Class type); /** @@ -353,7 +386,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat *

    * If all content has been aggregated into a single {@link Buffer}, {@link #deserializeAggregated(Buffer, Class)} * can be used. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Iterable, BufferAllocator)}. * @param source {@link BlockingIterable} containing {@link Buffer}s to deserialize. * @param type The class for {@link T}, the object to be deserialized. * @param The data type to deserialize. @@ -361,6 +395,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @return A transformed {@link BlockingIterable} such that each contained element in the original * {@link BlockingIterable} is transformed from type {@link Buffer} to an instance of {@link T}. */ + @Deprecated BlockingIterable deserialize(BlockingIterable source, Class type); /** @@ -374,7 +409,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * will eventually throw a {@link SerializationException} from the {@link CloseableIterator} returned by * {@link CloseableIterable#iterator()}. In such a case, all deserialized data will first be returned from the * {@link CloseableIterator}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingDeserializer} that understands your protocol's + * framing. * @param serializedData A {@link Buffer} containing serialized representation of one or more instances of * {@link T}. * @param type The class for {@link T}, the object to be deserialized. @@ -386,6 +422,7 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * {@link CloseableIterator} returned by {@link CloseableIterable#iterator()}. In such a case, all deserialized * data will first be returned from the {@link Iterator}. */ + @Deprecated CloseableIterable deserializeAggregated(Buffer serializedData, Class type); /** @@ -399,7 +436,8 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * will eventually throw a {@link SerializationException} from the {@link CloseableIterator} returned by * {@link CloseableIterable#iterator()}. In such a case, all deserialized data will first be returned from the * {@link CloseableIterator}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingDeserializer} that understands your protocol's + * framing. * @param serializedData A {@link Buffer} containing serialized representation of one or more instances of * {@link T}. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be deserialized. @@ -411,11 +449,12 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * {@link CloseableIterator} returned by {@link CloseableIterable#iterator()}. In such a case, all deserialized * data will first be returned from the {@link CloseableIterator}. */ + @Deprecated CloseableIterable deserializeAggregated(Buffer serializedData, TypeHolder typeHolder); /** * Deserializes the passed encoded {@link Buffer} to a single instance of {@link T}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.Deserializer}. * @param serializedData A {@link Buffer} containing serialized representation of a single instance of {@link T}. * @param type The class for {@link T}, the object to be deserialized. * @param The data type to deserialize. @@ -425,11 +464,12 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @throws SerializationException If the passed {@link Buffer} contains an incomplete object or if there is any * left over data in the {@link Buffer} after the deserialization is complete. */ + @Deprecated T deserializeAggregatedSingle(Buffer serializedData, Class type); /** * Deserializes the passed encoded {@link Buffer} to a single instance of {@link T}. - * + * @deprecated Use {@link io.servicetalk.serializer.api.Deserializer}. * @param serializedData A {@link Buffer} containing serialized representation of a single instance of {@link T}. * @param typeHolder {@link TypeHolder} holding the {@link ParameterizedType} to be deserialized. * @param The data type to deserialize. @@ -439,5 +479,6 @@ BlockingIterable serialize(BlockingIterable source, BufferAllocat * @throws SerializationException If the passed {@link Buffer} contains an incomplete object or if there is any * left over data in the {@link Buffer} after the deserialization is complete. */ + @Deprecated T deserializeAggregatedSingle(Buffer serializedData, TypeHolder typeHolder); } diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingDeserializer.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingDeserializer.java index 6c7ea2cb08..0515a72c0c 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingDeserializer.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingDeserializer.java @@ -16,6 +16,7 @@ package io.servicetalk.serialization.api; import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; import io.servicetalk.concurrent.BlockingIterable; import io.servicetalk.concurrent.BlockingIterator; import io.servicetalk.concurrent.GracefulAutoCloseable; @@ -35,9 +36,10 @@ * data is available. *

    * Implementations are assumed to be synchronous. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingDeserializer}. * @param Type of object to be deserialized. */ +@Deprecated public interface StreamingDeserializer extends GracefulAutoCloseable { /** @@ -49,11 +51,13 @@ public interface StreamingDeserializer extends GracefulAutoCloseable { * It is assumed that a single instance of {@link StreamingDeserializer} may receive calls to both this method and * {@link #deserialize(Iterable)}. Any left over data from one call is used by a subsequent call to the same or * different method. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingDeserializer} that understands your protocol's + * framing. * @param toDeserialize {@link Buffer} to deserialize. * @return {@link Iterable} containing zero or more deserialized instances of {@link T}, if any can be deserialized * from the data received till now. */ + @Deprecated Iterable deserialize(Buffer toDeserialize); /** @@ -65,11 +69,13 @@ public interface StreamingDeserializer extends GracefulAutoCloseable { * It is assumed that a single instance of {@link StreamingDeserializer} may receive calls to both this method and * {@link #deserialize(Buffer)}. Any left over data from one call is used by a subsequent call to the same or * different method. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Iterable, BufferAllocator)}. * @param toDeserialize {@link Iterable} of {@link Buffer}s to deserialize. * @return {@link Iterable} containing zero or more deserialized instances of {@link T}, if any can be deserialized * from the data received till now. */ + @Deprecated default Iterable deserialize(Iterable toDeserialize) { List deserialized = new ArrayList<>(2); for (Buffer buffer : toDeserialize) { @@ -90,11 +96,13 @@ default Iterable deserialize(Iterable toDeserialize) { * It is assumed that a single instance of {@link StreamingDeserializer} may receive calls to both this method and * {@link #deserialize(Buffer)}. Any left over data from one call is used by a subsequent call to the same or * different method. - * + * @deprecated Use + * {@link io.servicetalk.serializer.api.StreamingDeserializer#deserialize(Iterable, BufferAllocator)}. * @param toDeserialize {@link BlockingIterable} of {@link Buffer}s to deserialize. * @return {@link BlockingIterable} containing zero or more deserialized instances of {@link T}, if any can be * deserialized from the data received till now. */ + @Deprecated default BlockingIterable deserialize(BlockingIterable toDeserialize) { return new BlockingIterableFlatMap<>(toDeserialize, this::deserialize); } diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingSerializer.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingSerializer.java index c2509b1612..5e8c274bc7 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingSerializer.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/StreamingSerializer.java @@ -26,15 +26,18 @@ * A {@link StreamingSerializer} implementation may chose to be stateful or stateless. This contract does not assume * either. * Implementations are assumed to be synchronous. + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer}. */ +@Deprecated @FunctionalInterface public interface StreamingSerializer { /** * Serializes the passed {@link Object} {@code toSerialize} into the passed {@link Buffer} synchronously. - * + * @deprecated Use {@link io.servicetalk.serializer.api.StreamingSerializer}. * @param toSerialize {@link Object} to serialize. * @param destination {@link Buffer} to which the serialized representation of {@code toSerialize} is to be written. */ + @Deprecated void serialize(Object toSerialize, Buffer destination); } diff --git a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/TypeHolder.java b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/TypeHolder.java index 325cec6003..b3aa69bc63 100644 --- a/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/TypeHolder.java +++ b/servicetalk-serialization-api/src/main/java/io/servicetalk/serialization/api/TypeHolder.java @@ -27,12 +27,14 @@ *

      *  TypeHolder<Set<String>> holder = new TypeHolder<Set<String>>() { };
      * 
    - * + * @deprecated General {@link Type} serialization is not supported by all serializers. Defer + * to your specific {@link io.servicetalk.serializer.api.Serializer} implementation. * This implementation is based on the samples provided in * this article.. * * @param Type to be inferred. */ +@Deprecated public abstract class TypeHolder { private final Type type; diff --git a/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerDeserializationTest.java b/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerDeserializationTest.java index a631bde622..038edc49ad 100644 --- a/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerDeserializationTest.java +++ b/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerDeserializationTest.java @@ -53,8 +53,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +@Deprecated class DefaultSerializerDeserializationTest { - private static final TypeHolder> TYPE_FOR_LIST = new TypeHolder>() { }; diff --git a/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerSerializationTest.java b/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerSerializationTest.java index 569f6ba799..eb33f8f64a 100644 --- a/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerSerializationTest.java +++ b/servicetalk-serialization-api/src/test/java/io/servicetalk/serialization/api/DefaultSerializerSerializationTest.java @@ -46,8 +46,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +@Deprecated class DefaultSerializerSerializationTest { - private static final TypeHolder> TYPE_FOR_LIST = new TypeHolder>() { }; private IntUnaryOperator sizeEstimator; private List createdBuffers; diff --git a/servicetalk-serializer-api/build.gradle b/servicetalk-serializer-api/build.gradle new file mode 100644 index 0000000000..16d60f0b59 --- /dev/null +++ b/servicetalk-serializer-api/build.gradle @@ -0,0 +1,28 @@ +/* + * Copyright © 2021 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. + */ + +apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" + +dependencies { + api project(":servicetalk-buffer-api") + api project(":servicetalk-concurrent-api") + api project(":servicetalk-oio-api") + + implementation project(":servicetalk-annotations") + implementation project(":servicetalk-concurrent-api-internal") + implementation project(":servicetalk-oio-api-internal") + implementation "com.google.code.findbugs:jsr305:$jsr305Version" +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/Deserializer.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/Deserializer.java new file mode 100644 index 0000000000..e8ee161205 --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/Deserializer.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2021 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.serializer.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; + +/** + * Deserialize objects from {@link Buffer} to {@link T}. + * @param The type of objects that can be deserialized. + */ +@FunctionalInterface +public interface Deserializer { + /** + * Deserialize the contents from the {@link Buffer} parameter. + *

    + * The caller is responsible for assuming the buffer contents contains enough {@link Buffer#readableBytes()} to + * successfully deserialize. + * @param serializedData {@link Buffer} whose {@link Buffer#readableBytes()} contains a serialized object. The + * {@link Buffer#readerIndex()} will be advanced to indicate the content which has been consumed. + * @param allocator Used to allocate intermediate {@link Buffer}s if required. + * @return The result of the deserialization. + */ + T deserialize(Buffer serializedData, BufferAllocator allocator); +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/SerializationException.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/SerializationException.java new file mode 100644 index 0000000000..7855680ea0 --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/SerializationException.java @@ -0,0 +1,63 @@ +/* + * 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.serializer.api; + +/** + * Exception indicating an error during serialization or deserialization. + */ +public class SerializationException extends RuntimeException { + private static final long serialVersionUID = -7559655605112558344L; + + /** + * New instance. + * + * @param message for the exception. + */ + public SerializationException(final String message) { + super(message); + } + + /** + * New instance. + * + * @param message for the exception. + * @param cause for this exception. + */ + public SerializationException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * New instance. + * + * @param cause for this exception. + */ + public SerializationException(final Throwable cause) { + super(cause); + } + + /** + * New instance. + * @param message for the exception. + * @param cause for this exception. + * @param enableSuppression whether or not suppression is enabled or disabled. + * @param writableStackTrace whether or not the stack trace should be writable. + */ + public SerializationException(final String message, final Throwable cause, final boolean enableSuppression, + final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/Serializer.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/Serializer.java new file mode 100644 index 0000000000..0d6ceda998 --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/Serializer.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2021 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.serializer.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; + +/** + * Serialize from {@link T} to {@link Buffer}. + * @param The type of objects that can be serialized. + */ +@FunctionalInterface +public interface Serializer { + /** + * Serialize the {@link T} parameter to the {@link Buffer} parameter. + * @param toSerialize The {@link T} to serialize. + * @param allocator Used to allocate intermediate {@link Buffer}s if required. + * @param buffer Where the results of the serialization will be written to. + */ + void serialize(T toSerialize, BufferAllocator allocator, Buffer buffer); + + /** + * Serialize the {@link T} parameter to a {@link Buffer}. + * @param toSerialize The {@link T} to serialize. + * @param allocator Used to allocate the buffer to serialize to. + * @return The results of the serialization. + */ + default Buffer serialize(T toSerialize, BufferAllocator allocator) { + Buffer b = allocator.newBuffer(); + serialize(toSerialize, allocator, b); + return b; + } +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/SerializerDeserializer.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/SerializerDeserializer.java new file mode 100644 index 0000000000..6a206c01d3 --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/SerializerDeserializer.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2021 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.serializer.api; + +/** + * Both a {@link Serializer} and {@link Deserializer}. + * @param The type to serialize and deserialize. + */ +public interface SerializerDeserializer extends Serializer, Deserializer { +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingDeserializer.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingDeserializer.java new file mode 100644 index 0000000000..c1e72288d6 --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingDeserializer.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2021 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.serializer.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; + +import static io.servicetalk.concurrent.api.Publisher.fromIterable; + +/** + * Deserialize a {@link Publisher} of {@link Buffer} to {@link Publisher} of {@link T}. + * @param The type of objects that can be deserialized. + */ +@FunctionalInterface +public interface StreamingDeserializer { + /** + * Deserialize a {@link Publisher} of {@link Buffer} into a {@link Publisher} of {@link T}. + * @param serializedData the serialized stream of data represented in a {@link Publisher} of {@link Buffer}. + * @param allocator the {@link BufferAllocator} to use if allocation is required. + * @return The deserialized {@link Publisher} of {@link T}s. + */ + Publisher deserialize(Publisher serializedData, BufferAllocator allocator); + + /** + * Deserialize a {@link Iterable} of {@link Buffer} into a {@link Iterable} of {@link T}. + * @param serializedData the serialized stream data represented in a {@link Iterable} of {@link Buffer}. + * @param allocator the {@link BufferAllocator} to use if allocation is required. + * @return The deserialized {@link Iterable} of {@link T}s. + */ + default BlockingIterable deserialize(Iterable serializedData, BufferAllocator allocator) { + return deserialize(fromIterable(serializedData), allocator).toIterable(); + } +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializer.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializer.java new file mode 100644 index 0000000000..1a1ad236bd --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializer.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2021 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.serializer.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.BlockingIterable; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.oio.api.PayloadWriter; + +import static io.servicetalk.concurrent.api.Publisher.fromIterable; + +/** + * Serialize a {@link Publisher} of {@link T} to {@link Publisher} of {@link Buffer}. + * @param The type of objects that can be serialized. + */ +@FunctionalInterface +public interface StreamingSerializer { + /** + * Serialize a {@link Publisher} of {@link T}s into a {@link Publisher} of {@link Buffer}. + * @param toSerialize the deserialized stream of data represented in a {@link Publisher} of {@link T}. + * @param allocator the {@link BufferAllocator} to use if allocation is required. + * @return the serialized stream of data represented in a {@link Publisher} of {@link Buffer}. + */ + Publisher serialize(Publisher toSerialize, BufferAllocator allocator); + + /** + * Serialize a {@link Iterable} of {@link T}s into a {@link Iterable} of {@link Buffer}. + * @param toSerialize the deserialized stream of data represented in a {@link Iterable} of {@link T}. + * @param allocator the {@link BufferAllocator} to use if allocation is required. + * @return the serialized stream of data represented in a {@link Iterable} of {@link Buffer}. + */ + default BlockingIterable serialize(Iterable toSerialize, BufferAllocator allocator) { + return serialize(fromIterable(toSerialize), allocator).toIterable(); + } + + /** + * Serialize a {@link PayloadWriter} of {@link T}s into a {@link PayloadWriter} of {@link Buffer}. + * @param writer The {@link PayloadWriter} used to write the result of serialization to. + * @param allocator the {@link BufferAllocator} to use if allocation is required. + * @return a {@link PayloadWriter} where you can write {@link T}s to. + */ + default PayloadWriter serialize(PayloadWriter writer, BufferAllocator allocator) { + return StreamingSerializerUtils.serialize(this, writer, allocator); + } +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializerDeserializer.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializerDeserializer.java new file mode 100644 index 0000000000..42cf24af4a --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializerDeserializer.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2021 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.serializer.api; + +/** + * Both a {@link StreamingSerializer} and {@link StreamingDeserializer}. + * @param The type to serialize and deserialize. + */ +public interface StreamingSerializerDeserializer extends StreamingSerializer, StreamingDeserializer { +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializerUtils.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializerUtils.java new file mode 100644 index 0000000000..c28593beaf --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/StreamingSerializerUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2021 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.serializer.api; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.internal.ConnectablePayloadWriter; +import io.servicetalk.oio.api.PayloadWriter; + +import java.io.IOException; + +import static java.util.Objects.requireNonNull; + +final class StreamingSerializerUtils { + private StreamingSerializerUtils() { + } + + static PayloadWriter serialize(StreamingSerializer serializer, PayloadWriter writer, + BufferAllocator allocator) { + ConnectablePayloadWriter connectablePayloadWriter = new ConnectablePayloadWriter<>(); + serializer.serialize(connectablePayloadWriter.connect(), allocator) + // forEach is safe because: + // - backpressure is applied on thread calling PayloadWriter methods + // - terminal events are delivered via the returned PayloadWriter to writer + .forEach(buffer -> { + try { + writer.write(requireNonNull(buffer)); + } catch (IOException e) { + throw new SerializationException(e); + } + }); + return new PayloadWriter() { + @Override + public void write(final T t) throws IOException { + connectablePayloadWriter.write(t); + } + + @Override + public void close(final Throwable cause) throws IOException { + try { + connectablePayloadWriter.close(cause); + } finally { + writer.close(cause); + } + } + + @Override + public void close() throws IOException { + try { + connectablePayloadWriter.close(); + } finally { + writer.close(); + } + } + + @Override + public void flush() throws IOException { + try { + connectablePayloadWriter.flush(); + } finally { + writer.flush(); + } + } + }; + } +} diff --git a/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/package-info.java b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/package-info.java new file mode 100644 index 0000000000..528f208379 --- /dev/null +++ b/servicetalk-serializer-api/src/main/java/io/servicetalk/serializer/api/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2021 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. + */ +/** + * Serializer and deserializer APIs. + */ +@ElementsAreNonnullByDefault +package io.servicetalk.serializer.api; + +import io.servicetalk.annotations.ElementsAreNonnullByDefault; diff --git a/servicetalk-serializer-utils/build.gradle b/servicetalk-serializer-utils/build.gradle new file mode 100644 index 0000000000..9a3593a551 --- /dev/null +++ b/servicetalk-serializer-utils/build.gradle @@ -0,0 +1,37 @@ +/* + * Copyright © 2021 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. + */ + +apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" + +dependencies { + api project(":servicetalk-serializer-api") + api project(":servicetalk-buffer-api") + + implementation project(":servicetalk-annotations") + implementation project(":servicetalk-concurrent-internal") + implementation project(":servicetalk-utils-internal") + implementation "com.google.code.findbugs:jsr305:$jsr305Version" + + testImplementation testFixtures(project(":servicetalk-concurrent-api")) + testImplementation testFixtures(project(":servicetalk-concurrent-internal")) + testImplementation project(":servicetalk-concurrent-test-internal") + testImplementation project(":servicetalk-buffer-netty") + testImplementation "com.google.protobuf:protobuf-java:$protobufVersion" + testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5Version" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + testImplementation "org.mockito:mockito-core:$mockitoCoreVersion" +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/AbstractStringSerializer.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/AbstractStringSerializer.java new file mode 100644 index 0000000000..dfd778e69e --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/AbstractStringSerializer.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static io.servicetalk.utils.internal.CharsetUtils.standardCharsets; + +abstract class AbstractStringSerializer implements SerializerDeserializer { + private static final Map MAX_BYTES_PER_CHAR_MAP; + static { + Collection charsets = standardCharsets(); + MAX_BYTES_PER_CHAR_MAP = new HashMap<>(charsets.size()); + for (Charset charset : charsets) { + try { + MAX_BYTES_PER_CHAR_MAP.put(charset, (int) charset.newEncoder().maxBytesPerChar()); + } catch (Throwable ignored) { + // ignored + } + } + } + + private final Charset charset; + private final int maxBytesPerChar; + + /** + * Create a new instance. + * @param charset The charset used for encoding. + */ + AbstractStringSerializer(final Charset charset) { + this.charset = charset; + maxBytesPerChar = MAX_BYTES_PER_CHAR_MAP.getOrDefault(charset, 1); + } + + @Override + public final String deserialize(final Buffer serializedData, final BufferAllocator allocator) { + String result = serializedData.toString(charset); + serializedData.skipBytes(serializedData.readableBytes()); + return result; + } + + @Override + public final Buffer serialize(String toSerialize, BufferAllocator allocator) { + Buffer buffer = allocator.newBuffer(toSerialize.length() * maxBytesPerChar); + serialize(toSerialize, allocator, buffer); + return buffer; + } + + @Override + public void serialize(final String toSerialize, final BufferAllocator allocator, final Buffer buffer) { + buffer.writeCharSequence(toSerialize, charset); + } +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/ByteArraySerializer.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/ByteArraySerializer.java new file mode 100644 index 0000000000..3545cd4100 --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/ByteArraySerializer.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializerDeserializer; + +/** + * Serialize/deserialize {@code byte[]}. + */ +public final class ByteArraySerializer implements SerializerDeserializer { + private static final SerializerDeserializer BYTE_SERIALIZER = new ByteArraySerializer(false); + private static final SerializerDeserializer BYTE_SERIALIZER_COPY = new ByteArraySerializer(true); + + private final boolean forceCopy; + + private ByteArraySerializer(boolean forceCopy) { + this.forceCopy = forceCopy; + } + + /** + * Create a new instance. + * @param forceCopy {@code true} means that data will always be copied from {@link Buffer} memory. {@code false} + * means that if {@link Buffer#hasArray()} is {@code true} and the array offsets are aligned the result of + * serialization doesn't have to be copied. + * @return A serializer that produces/consumes {@code byte[]}. + */ + public static SerializerDeserializer byteArraySerializer(boolean forceCopy) { + return forceCopy ? BYTE_SERIALIZER_COPY : BYTE_SERIALIZER; + } + + @Override + public byte[] deserialize(final Buffer serializedData, final BufferAllocator allocator) { + // First try to return the raw underlying array, otherwise fallback to copy. + byte[] result; + if (!forceCopy && serializedData.hasArray() && serializedData.arrayOffset() == 0 && + (result = serializedData.array()).length == serializedData.readableBytes()) { + serializedData.skipBytes(result.length); + return result; + } + result = new byte[serializedData.readableBytes()]; + serializedData.readBytes(result); + return result; + } + + @Override + public Buffer serialize(byte[] toSerialize, BufferAllocator allocator) { + return allocator.wrap(toSerialize); + } + + @Override + public void serialize(final byte[] toSerialize, BufferAllocator allocator, final Buffer buffer) { + buffer.writeBytes(toSerialize); + } +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/FixedLengthStreamingSerializer.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/FixedLengthStreamingSerializer.java new file mode 100644 index 0000000000..8b3bc05e86 --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/FixedLengthStreamingSerializer.java @@ -0,0 +1,94 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import java.util.function.BiFunction; +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +import static java.lang.Integer.BYTES; +import static java.util.Objects.requireNonNull; +import static java.util.function.Function.identity; + +/** + * A {@link StreamingSerializerDeserializer} that uses a {@link SerializerDeserializer} and frames each object by + * preceding it with the length in bytes. The length component is fixed and always consumes 4 bytes. + * @param The type of object to serialize. + */ +public final class FixedLengthStreamingSerializer implements StreamingSerializerDeserializer { + private final SerializerDeserializer serializer; + private final ToIntFunction bytesEstimator; + + /** + * Create a new instance. + * @param serializer The {@link SerializerDeserializer} used to serialize/deserialize individual objects. + * @param bytesEstimator Provides the length in bytes for each {@link T} being serialized. + */ + public FixedLengthStreamingSerializer(final SerializerDeserializer serializer, + final ToIntFunction bytesEstimator) { + this.serializer = requireNonNull(serializer); + this.bytesEstimator = requireNonNull(bytesEstimator); + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return serializedData.liftSync(new FramedDeserializerOperator<>(serializer, LengthDeframer::new, allocator)) + .flatMapConcatIterable(identity()); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize.map(t -> { + Buffer buffer = allocator.newBuffer(BYTES + bytesEstimator.applyAsInt(t)); + final int beforeWriterIndex = buffer.writerIndex(); + buffer.writerIndex(beforeWriterIndex + BYTES); + serializer.serialize(t, allocator, buffer); + buffer.setInt(beforeWriterIndex, buffer.writerIndex() - beforeWriterIndex - BYTES); + return buffer; + }); + } + + private static final class LengthDeframer implements BiFunction { + private int expectedLength = -1; + + @Nullable + @Override + public Buffer apply(final Buffer buffer, final BufferAllocator allocator) { + if (expectedLength < 0) { + if (buffer.readableBytes() < BYTES) { + return null; + } + expectedLength = buffer.readInt(); + if (expectedLength < 0) { + throw new SerializationException("Invalid length: " + expectedLength); + } + } + if (buffer.readableBytes() < expectedLength) { + return null; + } + Buffer result = buffer.readBytes(expectedLength); + expectedLength = -1; + return result; + } + } +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/FramedDeserializerOperator.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/FramedDeserializerOperator.java new file mode 100644 index 0000000000..261f371cc4 --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/FramedDeserializerOperator.java @@ -0,0 +1,156 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.buffer.api.CompositeBuffer; +import io.servicetalk.concurrent.PublisherSource; +import io.servicetalk.concurrent.PublisherSource.Subscriber; +import io.servicetalk.concurrent.PublisherSource.Subscription; +import io.servicetalk.concurrent.api.PublisherOperator; +import io.servicetalk.concurrent.internal.ConcurrentSubscription; +import io.servicetalk.serializer.api.Deserializer; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.StreamingDeserializer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** + * Utility which helps implementations of {@link StreamingDeserializer} leverage a {@link Deserializer} and apply a + * framing to define the boundaries of each object. + * @param The type to serialize/deserialize. + */ +public final class FramedDeserializerOperator implements PublisherOperator> { + private final Deserializer deserializer; + private final BufferAllocator allocator; + private final Supplier> deframerSupplier; + + /** + * Create a new instance. + * @param deserializer The {@link Deserializer} to deserialize each individual item. + * @param deframerSupplier Provides a {@link Function} for each + * {@link PublisherSource#subscribe(Subscriber) subscribe} which is invoked each time a {@link Buffer} arrives. The + * {@link Function} is expected to return the a {@link Buffer} with enough data that the {@link Deserializer} can + * deserialize, or {@code null} if there isn't enough data. + * @param allocator Used to allocate {@link Buffer}s to aggregate data across deserialization calls if necessary. + */ + public FramedDeserializerOperator(final Deserializer deserializer, + final Supplier> deframerSupplier, + final BufferAllocator allocator) { + this.deserializer = requireNonNull(deserializer); + this.allocator = requireNonNull(allocator); + this.deframerSupplier = requireNonNull(deframerSupplier); + } + + @Override + public Subscriber apply(final Subscriber> subscriber) { + return new FramedSubscriber(subscriber, deframerSupplier.get()); + } + + private final class FramedSubscriber implements Subscriber { + @Nullable + private Subscription subscription; + @Nullable + private CompositeBuffer compositeBuffer; + private final BiFunction deframer; + private final Subscriber> subscriber; + + FramedSubscriber(final Subscriber> subscriber, + final BiFunction deframer) { + this.deframer = requireNonNull(deframer); + this.subscriber = subscriber; + } + + @Override + public void onSubscribe(final Subscription subscription) { + this.subscription = ConcurrentSubscription.wrap(subscription); + subscriber.onSubscribe(this.subscription); + } + + @Override + public void onNext(@Nullable final Buffer buffer) { + assert subscription != null; + if (buffer == null) { + subscription.request(1); + } else if (compositeBuffer != null && compositeBuffer.readableBytes() != 0) { + compositeBuffer.addBuffer(buffer); + doDeserialize(compositeBuffer); + } else { + doDeserialize(buffer); + } + } + + @Override + public void onError(final Throwable t) { + subscriber.onError(t); + } + + @Override + public void onComplete() { + if (compositeBuffer != null && compositeBuffer.readableBytes() != 0) { + subscriber.onError(new SerializationException("Deserialization completed with " + + compositeBuffer.readableBytes() + " remaining bytes")); + } else { + subscriber.onComplete(); + } + } + + private void doDeserialize(final Buffer input) { + assert subscription != null; + Buffer buff = deframer.apply(input, allocator); + if (buff != null) { + Buffer buff2 = deframer.apply(input, allocator); + final List result; + if (buff2 == null) { + result = singletonList(deserializer.deserialize(buff, allocator)); + } else { + result = new ArrayList<>(3); + result.add(deserializer.deserialize(buff, allocator)); + do { + result.add(deserializer.deserialize(buff2, allocator)); + } while ((buff2 = deframer.apply(input, allocator)) != null); + } + if (input == compositeBuffer) { + compositeBuffer.discardSomeReadBytes(); + } else if (input.readableBytes() != 0) { + addBuffer(input); + } + subscriber.onNext(result); + } else { + if (input != compositeBuffer) { + addBuffer(input); + } + subscription.request(1); + } + } + + private void addBuffer(Buffer buffer) { + if (compositeBuffer == null) { + compositeBuffer = allocator.newCompositeBuffer(Integer.MAX_VALUE); + } + compositeBuffer.addBuffer(buffer, true); + } + } +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/StringSerializer.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/StringSerializer.java new file mode 100644 index 0000000000..e460825e5c --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/StringSerializer.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.serializer.api.SerializerDeserializer; + +import java.nio.charset.Charset; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Serialize/deserialize {@link String}s encoded with a {@link Charset}. + */ +public final class StringSerializer extends AbstractStringSerializer { + private static final SerializerDeserializer UTF_8_SERIALIZER = new AbstractStringSerializer(UTF_8) { + @Override + public void serialize(final String toSerialize, final BufferAllocator allocator, final Buffer buffer) { + buffer.writeUtf8(toSerialize); + } + }; + private static final SerializerDeserializer US_ASCII_SERIALIZER = new AbstractStringSerializer(US_ASCII) { + @Override + public void serialize(final String toSerialize, final BufferAllocator allocator, final Buffer buffer) { + buffer.writeAscii(toSerialize); + } + }; + + /** + * Create a new instance. + * @param charset The charset used for encoding. + */ + private StringSerializer(final Charset charset) { + super(charset); + } + + /** + * Create a new instance. + * @param charset The charset used for encoding. + * @return A serializer that uses {@code charset} for encoding. + */ + public static SerializerDeserializer stringSerializer(final Charset charset) { + if (UTF_8.equals(charset)) { + return UTF_8_SERIALIZER; + } else if (US_ASCII.equals(charset)) { + return US_ASCII_SERIALIZER; + } + return new StringSerializer(charset); + } +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/VarIntLengthStreamingSerializer.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/VarIntLengthStreamingSerializer.java new file mode 100644 index 0000000000..06a7ca7bdc --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/VarIntLengthStreamingSerializer.java @@ -0,0 +1,187 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.buffer.api.BufferAllocator; +import io.servicetalk.concurrent.api.Publisher; +import io.servicetalk.serializer.api.SerializationException; +import io.servicetalk.serializer.api.SerializerDeserializer; +import io.servicetalk.serializer.api.StreamingSerializerDeserializer; + +import java.util.function.BiFunction; +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +import static java.lang.Math.min; +import static java.util.Objects.requireNonNull; +import static java.util.function.Function.identity; + +/** + * A {@link StreamingSerializerDeserializer} that uses a {@link SerializerDeserializer} and frames each object by + * preceding it with the length in bytes. The length component is variable length and encoded as + * base 128 VarInt. + * @param The type of object to serialize. + */ +public final class VarIntLengthStreamingSerializer implements StreamingSerializerDeserializer { + // 0xxx xxxx + static final int ONE_BYTE_VAL = 1 << 7; + // 1xxx xxxx 0xxx xxxx + static final int TWO_BYTE_VAL = 1 << 14; + // 1xxx xxxx 0xxx xxxx 0xxx xxxx + static final int THREE_BYTE_VAL = 1 << 21; + // 1xxx xxxx 0xxx xxxx 0xxx xxxx 0xxx xxxx + static final int FOUR_BYTE_VAL = 1 << 28; + static final int MAX_LENGTH_BYTES = 5; + private final SerializerDeserializer serializer; + private final ToIntFunction bytesEstimator; + + /** + * Create a new instance. + * + * @param serializer The {@link SerializerDeserializer} used to serialize/deserialize individual objects. + * @param bytesEstimator Estimates the length in bytes for each {@link T} being serialized. + */ + public VarIntLengthStreamingSerializer(final SerializerDeserializer serializer, + final ToIntFunction bytesEstimator) { + this.serializer = requireNonNull(serializer); + this.bytesEstimator = requireNonNull(bytesEstimator); + } + + @Override + public Publisher deserialize(final Publisher serializedData, final BufferAllocator allocator) { + return serializedData.liftSync(new FramedDeserializerOperator<>(serializer, LengthDeframer::new, allocator)) + .flatMapConcatIterable(identity()); + } + + @Override + public Publisher serialize(final Publisher toSerialize, final BufferAllocator allocator) { + return toSerialize.map(t -> { + Buffer buffer = allocator.newBuffer(MAX_LENGTH_BYTES + bytesEstimator.applyAsInt(t)); + final int beforeWriterIndex = buffer.writerIndex(); + buffer.writerIndex(beforeWriterIndex + MAX_LENGTH_BYTES); + serializer.serialize(t, allocator, buffer); + final int length = buffer.writerIndex() - beforeWriterIndex - MAX_LENGTH_BYTES; + setVarInt(length, buffer, beforeWriterIndex); + return buffer; + }); + } + + private static final class LengthDeframer implements BiFunction { + private int expectedLength = -1; + + @Nullable + @Override + public Buffer apply(final Buffer buffer, final BufferAllocator allocator) { + if (expectedLength < 0) { + expectedLength = getVarInt(buffer); + if (expectedLength < 0) { + return null; + } + } + if (buffer.readableBytes() < expectedLength) { + return null; + } + Buffer result = buffer.readBytes(expectedLength); + expectedLength = -1; + return result; + } + } + + static int getVarInt(Buffer buffer) { + final int maxBytesToInspect = min(MAX_LENGTH_BYTES, buffer.readableBytes()); + final int readerIndex = buffer.readerIndex(); + int i = 0; + for (; i < maxBytesToInspect; ++i) { + final byte b = buffer.getByte(i + readerIndex); + if ((b & 0x80) == 0) { + if (i == 0) { + return buffer.readByte(); + } else if (i == 1) { + final short varInt = buffer.readShort(); + return ((varInt & 0x7F) << 7) | + ((varInt & 0x7F00) >> 8); + } else if (i == 2) { + final int varInt = buffer.readMedium(); + return ((varInt & 0x7F) << 14) | + ((varInt & 0x7F00) >> 1) | + ((varInt & 0x7F0000) >> 16); + } else if (i == 3) { + final int varInt = buffer.readInt(); + return ((varInt & 0x7F) << 21) | + ((varInt & 0x7F00) << 6) | + ((varInt & 0x7F0000) >> 9) | + ((varInt & 0x7F000000) >> 24); + } else { + assert i == 4; + final byte b0 = buffer.readByte(); + final int varInt = buffer.readInt(); + if ((varInt & 0xF8) != 0) { + throw new SerializationException("java int cannot support larger than " + Integer.MAX_VALUE); + } + return ((varInt & 0x7) << 28) | + ((varInt & 0x7F00) << 13) | + ((varInt & 0x7F0000) >> 2) | + ((varInt & 0x7F000000) >> 17) | + (b0 & 0x7F); + } + } + } + if (i == MAX_LENGTH_BYTES) { + throw new SerializationException("java int cannot support more than " + MAX_LENGTH_BYTES + + " bytes of VarInt"); + } + + return -1; + } + + static void setVarInt(int val, Buffer buffer, int index) { + assert val >= 0; + if (val < ONE_BYTE_VAL) { + // The initial buffer allocation allows for MAX_LENGTH_BYTES space to encode the length because we don't + // know the length of the serialization until it is completed, and we want to avoid copying data to write + // the length prefix. So we write the length adjacent to the data and skip the bytes not required to encode + // the length. + buffer.setByte(index + 4, (byte) val).skipBytes(4); + } else if (val < TWO_BYTE_VAL) { + final int varIntEncoded = + 0x8000 | ((val & 0x7F) << 8) | + ((val & 0x3F80) >> 7); + buffer.setShort(index + 3, varIntEncoded).skipBytes(3); + } else if (val < THREE_BYTE_VAL) { + final int varIntEncoded = + 0x800000 | ((val & 0x7F) << 16) | + 0x8000 | ((val & 0x3F80) << 1) | + ((val & 0x1FC000) >> 14); + buffer.setMedium(index + 2, varIntEncoded).skipBytes(2); + } else if (val < FOUR_BYTE_VAL) { + final int varIntEncoded = + 0x80000000 | ((val & 0x7F) << 24) | + 0x800000 | ((val & 0x3F80) << 9) | + 0x8000 | ((val & 0x1FC000) >> 6) | + ((val & 0xFE00000) >> 21); + buffer.setInt(index + 1, varIntEncoded).skipBytes(1); + } else { + buffer.setByte(index, 0x80 | (val & 0x7F)); + final int varIntEncoded = + 0x80000000 | ((val & 0x3F80) << 17) | + 0x800000 | ((val & 0x1FC000) << 2) | + 0x8000 | ((val & 0xFE00000) >> 13) | + ((val & 0xF0000000) >> 28); + buffer.setInt(index + 1, varIntEncoded); + } + } +} diff --git a/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/package-info.java b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/package-info.java new file mode 100644 index 0000000000..b9b5f00fc1 --- /dev/null +++ b/servicetalk-serializer-utils/src/main/java/io/servicetalk/serializer/utils/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright © 2021 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. + */ +/** + * Serializer utilities. + */ +@ElementsAreNonnullByDefault +package io.servicetalk.serializer.utils; + +import io.servicetalk.annotations.ElementsAreNonnullByDefault; diff --git a/servicetalk-serializer-utils/src/test/java/io/servicetalk/serializer/utils/FixedLengthStreamingSerializerTest.java b/servicetalk-serializer-utils/src/test/java/io/servicetalk/serializer/utils/FixedLengthStreamingSerializerTest.java new file mode 100644 index 0000000000..1913c77773 --- /dev/null +++ b/servicetalk-serializer-utils/src/test/java/io/servicetalk/serializer/utils/FixedLengthStreamingSerializerTest.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import org.junit.jupiter.api.Test; + +import static io.servicetalk.buffer.netty.BufferAllocators.DEFAULT_ALLOCATOR; +import static io.servicetalk.concurrent.api.Publisher.from; +import static io.servicetalk.serializer.utils.StringSerializer.stringSerializer; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +class FixedLengthStreamingSerializerTest { + @Test + void serializeDeserialize() throws Exception { + FixedLengthStreamingSerializer serializer = new FixedLengthStreamingSerializer<>( + stringSerializer(UTF_8), String::length); + + assertThat(serializer.deserialize(serializer.serialize(from("foo", "bar"), DEFAULT_ALLOCATOR), + DEFAULT_ALLOCATOR).toFuture().get(), contains("foo", "bar")); + } +} diff --git a/servicetalk-serializer-utils/src/test/java/io/servicetalk/serializer/utils/VarIntLengthStreamingSerializerTest.java b/servicetalk-serializer-utils/src/test/java/io/servicetalk/serializer/utils/VarIntLengthStreamingSerializerTest.java new file mode 100644 index 0000000000..b743bccf33 --- /dev/null +++ b/servicetalk-serializer-utils/src/test/java/io/servicetalk/serializer/utils/VarIntLengthStreamingSerializerTest.java @@ -0,0 +1,137 @@ +/* + * Copyright © 2021 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.serializer.utils; + +import io.servicetalk.buffer.api.Buffer; +import io.servicetalk.serializer.api.SerializationException; + +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.CodedOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.stream.Stream; + +import static io.servicetalk.buffer.netty.BufferAllocators.DEFAULT_ALLOCATOR; +import static io.servicetalk.concurrent.api.Publisher.from; +import static io.servicetalk.serializer.utils.StringSerializer.stringSerializer; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.FOUR_BYTE_VAL; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.MAX_LENGTH_BYTES; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.ONE_BYTE_VAL; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.THREE_BYTE_VAL; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.TWO_BYTE_VAL; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.getVarInt; +import static io.servicetalk.serializer.utils.VarIntLengthStreamingSerializer.setVarInt; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VarIntLengthStreamingSerializerTest { + @Test + void decodeThrowsIfMoreThanMaxBytes() { + Buffer buffer = DEFAULT_ALLOCATOR.newBuffer(MAX_LENGTH_BYTES); + byte nonFinalByte = (byte) 0x80; + for (int i = 0; i <= MAX_LENGTH_BYTES; ++i) { + buffer.writeByte(nonFinalByte); + } + + assertThrows(SerializationException.class, () -> getVarInt(buffer)); + } + + @Test + void decodeThrowsIfGreaterThanMaxInt() { + Buffer buffer = DEFAULT_ALLOCATOR.newBuffer(MAX_LENGTH_BYTES); + byte nonFinalByte = (byte) 0xFF; + for (int i = 0; i < MAX_LENGTH_BYTES - 1; ++i) { + buffer.writeByte(nonFinalByte); + } + buffer.writeByte((byte) 0xF); + + assertThrows(SerializationException.class, () -> getVarInt(buffer)); + } + + @ParameterizedTest(name = "val={0}") + @MethodSource("boundaries") + void encodeAndDecodeBoundaries(int val) { + Buffer buffer = DEFAULT_ALLOCATOR.newBuffer(MAX_LENGTH_BYTES * 2) + .writerIndex(MAX_LENGTH_BYTES); // setVarInt expects to write before writer index, give it space. + setVarInt(val, buffer, buffer.readerIndex()); + assertThat(getVarInt(buffer), is(val)); + assertThat(buffer.readableBytes(), is(0)); + } + + @Test + void serializeDeserialize() throws Exception { + VarIntLengthStreamingSerializer serializer = new VarIntLengthStreamingSerializer<>( + stringSerializer(UTF_8), String::length); + + assertThat(serializer.deserialize(serializer.serialize(from("foo", "bar"), DEFAULT_ALLOCATOR), + DEFAULT_ALLOCATOR).toFuture().get(), contains("foo", "bar")); + } + + @ParameterizedTest(name = "val={0}") + @MethodSource("boundaries") + void protobufVarIntDecodeCompatibility(int val) throws IOException { + byte[] array = new byte[MAX_LENGTH_BYTES]; + CodedOutputStream oStream = CodedOutputStream.newInstance(array); + oStream.writeInt32NoTag(val); + Buffer buffer = DEFAULT_ALLOCATOR.wrap(array).writerIndex(array.length - oStream.spaceLeft()); + assertThat(getVarInt(buffer), is(val)); + } + + @ParameterizedTest(name = "val={0}") + @MethodSource("boundaries") + void protobufVarIntEncodeCompatibility(int val) throws IOException { + // setVarInt expects to write before writer index, give it space. + Buffer buffer = DEFAULT_ALLOCATOR.newBuffer(MAX_LENGTH_BYTES).writerIndex(MAX_LENGTH_BYTES); + setVarInt(val, buffer, buffer.readerIndex()); + + // Copy bytes into the destination array, so protobuf sees values starting at 0 index. + // setVarInt will encode the value packed to the right of the array, because the value is encoded into + // the array first and we want to avoid copying/moving serialized bytes just to write the size. + byte[] array = new byte[buffer.readableBytes()]; + buffer.readBytes(array); + + assertThat(CodedInputStream.newInstance(array).readInt32(), is(val)); + } + + @SuppressWarnings("unused") + private static Stream boundaries() { + return Stream.of( + Arguments.of(0), + Arguments.of(1), + Arguments.of(ONE_BYTE_VAL - 1), + Arguments.of(ONE_BYTE_VAL), + Arguments.of(ONE_BYTE_VAL + 1), + Arguments.of(TWO_BYTE_VAL - 1), + Arguments.of(TWO_BYTE_VAL), + Arguments.of(TWO_BYTE_VAL + 1), + Arguments.of(THREE_BYTE_VAL - 1), + Arguments.of(THREE_BYTE_VAL), + Arguments.of(THREE_BYTE_VAL + 1), + Arguments.of(FOUR_BYTE_VAL - 1), + Arguments.of(FOUR_BYTE_VAL), + Arguments.of(FOUR_BYTE_VAL + 1), + Arguments.of(Integer.MAX_VALUE - 1), + Arguments.of(Integer.MAX_VALUE) + ); + } +} diff --git a/servicetalk-utils-internal/src/main/java/io/servicetalk/utils/internal/CharsetUtils.java b/servicetalk-utils-internal/src/main/java/io/servicetalk/utils/internal/CharsetUtils.java new file mode 100644 index 0000000000..b84d0bc86f --- /dev/null +++ b/servicetalk-utils-internal/src/main/java/io/servicetalk/utils/internal/CharsetUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2021 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.utils.internal; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_16; +import static java.nio.charset.StandardCharsets.UTF_16BE; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Arrays.asList; + +/** + * {@link Charset} utilities. + */ +public final class CharsetUtils { + private static final Collection STANDARD_CHARSETS = + asList(US_ASCII, ISO_8859_1, UTF_8, UTF_16BE, UTF_16LE, UTF_16); + + private CharsetUtils() { + } + + /** + * Get a {@link Collection} of the {@link StandardCharsets}. + * @return a {@link Collection} of the {@link StandardCharsets}. + */ + public static Collection standardCharsets() { + return STANDARD_CHARSETS; + } +} diff --git a/settings.gradle b/settings.gradle index db81ba7f78..b36db07f59 100644 --- a/settings.gradle +++ b/settings.gradle @@ -86,6 +86,8 @@ include "servicetalk-annotations", "servicetalk-router-api", "servicetalk-router-utils-internal", "servicetalk-serialization-api", + "servicetalk-serializer-api", + "servicetalk-serializer-utils", "servicetalk-tcp-netty-internal", "servicetalk-test-resources", "servicetalk-transport-api",