com.palantir.conjure.java.undertow.runtime.ConjureBodySerDe Maven / Gradle / Ivy
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.palantir.conjure.java.undertow.runtime;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.palantir.conjure.java.undertow.lib.BinaryResponseBody;
import com.palantir.conjure.java.undertow.lib.BodySerDe;
import com.palantir.conjure.java.undertow.lib.Deserializer;
import com.palantir.conjure.java.undertow.lib.Endpoint;
import com.palantir.conjure.java.undertow.lib.Serializer;
import com.palantir.conjure.java.undertow.lib.TypeMarker;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.tracing.CloseableTracer;
import com.palantir.tracing.TagTranslator;
import com.palantir.tracing.Tracer;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HeaderValues;
import io.undertow.util.Headers;
import io.undertow.util.Protocols;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.util.List;
import java.util.Optional;
import org.xnio.IoUtils;
/** Package private internal API. */
final class ConjureBodySerDe implements BodySerDe {
private static final SafeLogger log = SafeLoggerFactory.get(ConjureBodySerDe.class);
private static final String BINARY_CONTENT_TYPE = "application/octet-stream";
private static final Splitter ACCEPT_VALUE_SPLITTER =
Splitter.on(',').trimResults().omitEmptyStrings();
private final List encodings;
/**
* Selects the first (based on input order) of the provided encodings that
* {@link Encoding#supportsContentType supports} the serialization format {@link Headers#ACCEPT accepted} by a given
* request, or the first serializer if no such serializer can be found.
*/
ConjureBodySerDe(List encodings) {
// Defensive copy
this.encodings =
encodings.stream().map(LazilyInitializedEncoding::new).collect(ImmutableList.toImmutableList());
Preconditions.checkArgument(encodings.size() > 0, "At least one Encoding is required");
}
@Override
public Serializer serializer(TypeMarker token) {
return new EncodingSerializerRegistry<>(encodings, token, Optional.empty());
}
@Override
public Serializer serializer(TypeMarker token, Endpoint endpoint) {
return new EncodingSerializerRegistry<>(encodings, token, Optional.of(endpoint));
}
@Override
public Deserializer deserializer(TypeMarker token) {
return new EncodingDeserializerRegistry<>(encodings, token, Optional.empty());
}
@Override
public Deserializer deserializer(TypeMarker token, Endpoint endpoint) {
return new EncodingDeserializerRegistry<>(encodings, token, Optional.of(endpoint));
}
@Override
public void serialize(BinaryResponseBody value, HttpServerExchange exchange) throws IOException {
Preconditions.checkNotNull(value, "A BinaryResponseBody value is required");
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, BINARY_CONTENT_TYPE);
Tracer.fastStartSpan(TracedEncoding.SERIALIZE_OPERATION);
try {
// We wrap the provided outputstream to prevent premature closure, otherwise a common pattern of
// using try-with-resources structures within BinaryResponseBody prevents failures from being
// understood by clients. try-with-resource ends up calling OutputStream.close before the
// exception handler is invoked, which tells the server (and then client) that the response bytes
// have been fully sent successfully.
value.write(new UnclosableOutputStream(exchange.getOutputStream()));
} finally {
Tracer.fastCompleteSpan(SerializeBinaryTagTranslator.INSTANCE, SerializeBinaryTagTranslator.INSTANCE);
}
}
@Override
public InputStream deserializeInputStream(HttpServerExchange exchange) {
String contentType = getContentType(exchange);
// Compare using 'String#regionMatches' to avoid allocation
if (contentType.length() < BINARY_CONTENT_TYPE.length()
|| !contentType.regionMatches(
/* ignoreCase = */ true, 0, BINARY_CONTENT_TYPE, 0, BINARY_CONTENT_TYPE.length())) {
throw FrameworkException.unsupportedMediaType(
"Unsupported Content-Type", SafeArg.of("Content-Type", contentType));
}
return exchange.getInputStream();
}
private static final class EncodingSerializerRegistry implements Serializer {
private final EncodingSerializerContainer defaultEncoding;
private final List> encodings;
EncodingSerializerRegistry(List encodings, TypeMarker token, Optional endpoint) {
this.encodings = encodings.stream()
.map(encoding -> new EncodingSerializerContainer<>(encoding, token, endpoint))
.collect(ImmutableList.toImmutableList());
this.defaultEncoding = this.encodings.get(0);
}
@Override
public void serialize(T value, HttpServerExchange exchange) throws IOException {
Preconditions.checkNotNull(value, "cannot serialize null value");
safelyDrainRequestBody(exchange);
EncodingSerializerContainer container = getResponseSerializer(exchange);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, container.encoding.getContentType());
container.serializer.serialize(value, exchange.getOutputStream());
}
/** Returns the {@link EncodingSerializerContainer} to use for the exchange response. */
@SuppressWarnings("ForLoopReplaceableByForEach") // performance sensitive code avoids iterator allocation
EncodingSerializerContainer getResponseSerializer(HttpServerExchange exchange) {
HeaderValues acceptValues = exchange.getRequestHeaders().get(Headers.ACCEPT);
if (acceptValues != null) {
// This implementation prefers the client "Accept" order
for (int i = 0; i < acceptValues.size(); i++) {
for (String acceptValue : ACCEPT_VALUE_SPLITTER.split(acceptValues.get(i))) {
for (int j = 0; j < encodings.size(); j++) {
EncodingSerializerContainer container = encodings.get(j);
if (container.encoding.supportsContentType(acceptValue)) {
return container;
}
}
}
}
}
// Fall back to the default
return defaultEncoding;
}
}
private static final class EncodingSerializerContainer {
private final Encoding encoding;
private final Encoding.Serializer serializer;
EncodingSerializerContainer(Encoding encoding, TypeMarker token, Optional endpoint) {
this.encoding = encoding;
this.serializer = endpoint.isPresent()
? TracedEncoding.wrap(encoding).serializer(token, endpoint.get())
: TracedEncoding.wrap(encoding).serializer(token);
}
}
private static final class EncodingDeserializerRegistry implements Deserializer {
private final List> encodings;
private final boolean optionalType;
private final TypeMarker marker;
EncodingDeserializerRegistry(List encodings, TypeMarker token, Optional endpoint) {
this.encodings = encodings.stream()
.map(encoding -> new EncodingDeserializerContainer<>(encoding, token, endpoint))
.collect(ImmutableList.toImmutableList());
this.optionalType = TypeMarkers.isOptional(token);
this.marker = token;
}
@Override
public T deserialize(HttpServerExchange exchange) throws IOException {
// If this deserializer is built for an optional root type, Optional>, OptionalInt, etc,
// and the incoming request body might be empty (does not have a content-length greater than zero)
// we must map from an empty request body to an empty optional.
// See https://github.com/palantir/conjure/blob/master/docs/spec/wire.md#23-body-parameter
if (optionalType && maybeEmptyBody(exchange)) {
return deserializeOptional(exchange);
}
return deserializeInternal(exchange, exchange.getInputStream());
}
private T deserializeOptional(HttpServerExchange exchange) throws IOException {
// If the first byte of the request stream is -1 (EOF) we return the empty optional type.
// We cannot provide the empty stream to jackson because there is no content for jackson
// to deserialize.
PushbackInputStream requestStream = new PushbackInputStream(exchange.getInputStream(), 1);
int firstByte = requestStream.read();
if (firstByte == -1) {
return TypeMarkers.getEmptyOptional(marker);
}
// Otherwise reset the request stream and deserialize normally.
requestStream.unread(firstByte);
return deserializeInternal(exchange, requestStream);
}
private T deserializeInternal(HttpServerExchange exchange, InputStream requestStream) throws IOException {
EncodingDeserializerContainer container = getRequestDeserializer(exchange);
return container.deserializer.deserialize(requestStream);
}
private static boolean maybeEmptyBody(HttpServerExchange exchange) {
// Content-Length maybe null if "Transfer-Encoding: chunked" is sent with a full body.
String contentLength = exchange.getRequestHeaders().getFirst(Headers.CONTENT_LENGTH);
return contentLength == null || "0".equals(contentLength);
}
/** Returns the {@link EncodingDeserializerContainer} to use to deserialize the request body. */
@SuppressWarnings("ForLoopReplaceableByForEach") // performance sensitive code avoids iterator allocation
EncodingDeserializerContainer getRequestDeserializer(HttpServerExchange exchange) {
String contentType = getContentType(exchange);
for (int i = 0; i < encodings.size(); i++) {
EncodingDeserializerContainer container = encodings.get(i);
if (container.encoding.supportsContentType(contentType)) {
return container;
}
}
throw FrameworkException.unsupportedMediaType(
"Unsupported Content-Type", SafeArg.of("Content-Type", contentType));
}
}
private static final class EncodingDeserializerContainer {
private final Encoding encoding;
private final Encoding.Deserializer deserializer;
EncodingDeserializerContainer(Encoding encoding, TypeMarker token, Optional endpoint) {
this.encoding = encoding;
this.deserializer = endpoint.isPresent()
? TracedEncoding.wrap(encoding).deserializer(token, endpoint.get())
: TracedEncoding.wrap(encoding).deserializer(token);
}
}
/**
* Ensure the client isn't blocked sending additional data. It's very uncommon for this to be necessary, in most
* cases exceptional responses are far below the 16k buffer threshold, not even considering socket buffers.
*/
private static void safelyDrainRequestBody(HttpServerExchange exchange) {
// No need to impact http/2 which supports out-of-band responses.
if ((Protocols.HTTP_1_1.equals(exchange.getProtocol()) || Protocols.HTTP_1_0.equals(exchange.getProtocol()))
&& !exchange.isRequestComplete()) {
try (CloseableTracer ignored = CloseableTracer.startSpan("Undertow: drain request body")) {
IoUtils.safeClose(exchange.getInputStream());
}
}
}
private enum SerializeBinaryTagTranslator implements TagTranslator
© 2015 - 2024 Weber Informatics LLC | Privacy Policy