All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.palantir.conjure.java.dialogue.serde.ConjureBodySerDe Maven / Gradle / Ivy

The newest version!
/*
 * (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.dialogue.serde;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.CaffeineSpec;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.net.HttpHeaders;
import com.palantir.dialogue.BinaryRequestBody;
import com.palantir.dialogue.BodySerDe;
import com.palantir.dialogue.Deserializer;
import com.palantir.dialogue.RequestBody;
import com.palantir.dialogue.Response;
import com.palantir.dialogue.Serializer;
import com.palantir.dialogue.TypeMarker;
import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.UnsafeArg;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.logsafe.exceptions.SafeRuntimeException;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/** Package private internal API. */
final class ConjureBodySerDe implements BodySerDe {

    private static final SafeLogger log = SafeLoggerFactory.get(ConjureBodySerDe.class);
    private final List encodingsSortedByWeight;
    private final Encoding defaultEncoding;
    private final Deserializer binaryInputStreamDeserializer;
    private final Deserializer> optionalBinaryInputStreamDeserializer;
    private final Deserializer emptyBodyDeserializer;
    private final LoadingCache> serializers;
    private final LoadingCache> deserializers;

    /**
     * Selects the first (based on input order) of the provided encodings that
     * {@link Encoding#supportsContentType supports} the serialization format {@link HttpHeaders#ACCEPT accepted}
     * by a given request, or the first serializer if no such serializer can be found.
     */
    ConjureBodySerDe(
            List rawEncodings,
            ErrorDecoder errorDecoder,
            EmptyContainerDeserializer emptyContainerDeserializer,
            CaffeineSpec cacheSpec) {
        List encodings = decorateEncodings(rawEncodings);
        this.encodingsSortedByWeight = sortByWeight(encodings);
        Preconditions.checkArgument(encodings.size() > 0, "At least one Encoding is required");
        this.defaultEncoding = encodings.get(0).encoding();
        this.binaryInputStreamDeserializer = new EncodingDeserializerRegistry<>(
                ImmutableList.of(BinaryEncoding.INSTANCE),
                errorDecoder,
                emptyContainerDeserializer,
                BinaryEncoding.MARKER);
        this.optionalBinaryInputStreamDeserializer = new EncodingDeserializerRegistry<>(
                ImmutableList.of(BinaryEncoding.INSTANCE),
                errorDecoder,
                emptyContainerDeserializer,
                BinaryEncoding.OPTIONAL_MARKER);
        this.emptyBodyDeserializer = new EmptyBodyDeserializer(errorDecoder);
        // Class unloading: Not supported, Jackson keeps strong references to the types
        // it sees: https://github.com/FasterXML/jackson-databind/issues/489
        this.serializers = Caffeine.from(cacheSpec)
                .build(type -> new EncodingSerializerRegistry<>(defaultEncoding, TypeMarker.of(type)));
        this.deserializers = Caffeine.from(cacheSpec)
                .build(type -> new EncodingDeserializerRegistry<>(
                        encodingsSortedByWeight, errorDecoder, emptyContainerDeserializer, TypeMarker.of(type)));
    }

    private static List decorateEncodings(List input) {
        return input.stream()
                .map(weightedEncoding -> WeightedEncoding.of(
                        new LazilyInitializedEncoding(new TracedEncoding(weightedEncoding.encoding())),
                        weightedEncoding.weight()))
                .collect(ImmutableList.toImmutableList());
    }

    private ImmutableList sortByWeight(List encodings) {
        // Use list.sort which guarantees a stable sort, so the original order is preserved
        // when weights are equal.
        List mutableEncodings = new ArrayList<>(encodings);
        mutableEncodings.sort(Comparator.comparing(WeightedEncoding::weight).reversed());
        return ImmutableList.copyOf(Lists.transform(mutableEncodings, WeightedEncoding::encoding));
    }

    @Override
    @SuppressWarnings("unchecked")
    public  Serializer serializer(TypeMarker token) {
        return (Serializer) serializers.get(token.getType());
    }

    @Override
    @SuppressWarnings("unchecked")
    public  Deserializer deserializer(TypeMarker token) {
        return (Deserializer) deserializers.get(token.getType());
    }

    @Override
    public Deserializer emptyBodyDeserializer() {
        return emptyBodyDeserializer;
    }

    @Override
    public Deserializer inputStreamDeserializer() {
        return binaryInputStreamDeserializer;
    }

    @Override
    public Deserializer> optionalInputStreamDeserializer() {
        return optionalBinaryInputStreamDeserializer;
    }

    @Override
    public RequestBody serialize(BinaryRequestBody value) {
        Preconditions.checkNotNull(value, "A BinaryRequestBody value is required");
        return new RequestBody() {

            @Override
            public void writeTo(OutputStream output) throws IOException {
                value.write(output);
            }

            @Override
            public String contentType() {
                return BinaryEncoding.CONTENT_TYPE;
            }

            @Override
            public boolean repeatable() {
                return value.repeatable();
            }

            @Override
            public void close() {
                try {
                    value.close();
                } catch (IOException | RuntimeException e) {
                    log.warn("Failed to close BinaryRequestBody {}", UnsafeArg.of("body", value), e);
                }
            }
        };
    }

    private static final class EncodingSerializerRegistry implements Serializer {

        private final EncodingSerializerContainer encoding;

        EncodingSerializerRegistry(Encoding encoding, TypeMarker token) {
            this.encoding = new EncodingSerializerContainer<>(encoding, token);
        }

        @Override
        public RequestBody serialize(T value) {
            Preconditions.checkNotNull(value, "cannot serialize null value");

            return new RequestBody() {

                @Override
                public void writeTo(OutputStream output) throws IOException {
                    encoding.serializer.serialize(value, output);
                }

                @Override
                public String contentType() {
                    return encoding.encoding.getContentType();
                }

                @Override
                public boolean repeatable() {
                    return true;
                }

                @Override
                public void close() {
                    // nop
                }
            };
        }
    }

    private static final class EncodingSerializerContainer {

        private final Encoding encoding;
        private final Encoding.Serializer serializer;

        EncodingSerializerContainer(Encoding encoding, TypeMarker token) {
            this.encoding = encoding;
            this.serializer = encoding.serializer(token);
        }
    }

    private static final class EncodingDeserializerRegistry implements Deserializer {

        private static final SafeLogger log = SafeLoggerFactory.get(EncodingDeserializerRegistry.class);
        private final ImmutableList> encodings;
        private final ErrorDecoder errorDecoder;
        private final Optional acceptValue;
        private final Supplier> emptyInstance;
        private final TypeMarker token;

        EncodingDeserializerRegistry(
                List encodings,
                ErrorDecoder errorDecoder,
                EmptyContainerDeserializer empty,
                TypeMarker token) {
            this.encodings = encodings.stream()
                    .map(encoding -> new EncodingDeserializerContainer<>(encoding, token))
                    .collect(ImmutableList.toImmutableList());
            this.errorDecoder = errorDecoder;
            this.token = token;
            this.emptyInstance = Suppliers.memoize(() -> empty.tryGetEmptyInstance(token));
            // Encodings are applied to the accept header in the order of preference based on the provided list.
            this.acceptValue =
                    Optional.of(encodings.stream().map(Encoding::getContentType).collect(Collectors.joining(", ")));
        }

        @Override
        public T deserialize(Response response) {
            boolean closeResponse = true;
            try {
                if (errorDecoder.isError(response)) {
                    throw errorDecoder.decode(response);
                } else if (response.code() == 204) {
                    // TODO(dfox): what if we get a 204 for a non-optional type???
                    // TODO(dfox): support http200 & body=null
                    // TODO(dfox): what if we were expecting an empty list but got {}?
                    Optional maybeEmptyInstance = emptyInstance.get();
                    if (maybeEmptyInstance.isPresent()) {
                        return maybeEmptyInstance.get();
                    }
                    throw new SafeRuntimeException(
                            "Unable to deserialize non-optional response type from 204", SafeArg.of("type", token));
                }

                Optional contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
                if (!contentType.isPresent()) {
                    throw new SafeIllegalArgumentException(
                            "Response is missing Content-Type header",
                            SafeArg.of("received", response.headers().keySet()));
                }
                Encoding.Deserializer deserializer = getResponseDeserializer(contentType.get());
                T deserialized = deserializer.deserialize(response.body());
                // deserializer has taken on responsibility for closing the response body
                closeResponse = false;
                return deserialized;
            } catch (IOException e) {
                throw new SafeRuntimeException(
                        "Failed to deserialize response stream",
                        e,
                        SafeArg.of("contentType", response.getFirstHeader(HttpHeaders.CONTENT_TYPE)),
                        SafeArg.of("type", token));
            } finally {
                if (closeResponse) {
                    response.close();
                }
            }
        }

        @Override
        public Optional accepts() {
            return acceptValue;
        }

        /** Returns the {@link EncodingDeserializerContainer} to use to deserialize the request body. */
        @SuppressWarnings("ForLoopReplaceableByForEach")
        // performance sensitive code avoids iterator allocation
        Encoding.Deserializer getResponseDeserializer(String contentType) {
            for (int i = 0; i < encodings.size(); i++) {
                EncodingDeserializerContainer container = encodings.get(i);
                if (container.encoding.supportsContentType(contentType)) {
                    return container.deserializer;
                }
            }
            return throwingDeserializer(contentType);
        }

        private Encoding.Deserializer throwingDeserializer(String contentType) {
            return new Encoding.Deserializer() {
                @Override
                public T deserialize(InputStream input) {
                    try {
                        input.close();
                    } catch (RuntimeException | IOException e) {
                        log.warn("Failed to close InputStream", e);
                    }
                    throw new SafeRuntimeException(
                            "Unsupported Content-Type",
                            SafeArg.of("received", contentType),
                            SafeArg.of("supportedEncodings", encodings));
                }
            };
        }
    }

    /** Effectively just a pair. */
    private static final class EncodingDeserializerContainer {

        private final Encoding encoding;
        private final Encoding.Deserializer deserializer;

        EncodingDeserializerContainer(Encoding encoding, TypeMarker token) {
            this.encoding = encoding;
            this.deserializer = encoding.deserializer(token);
        }

        @Override
        public String toString() {
            return "EncodingDeserializerContainer{encoding=" + encoding + ", deserializer=" + deserializer + '}';
        }
    }

    private static final class EmptyBodyDeserializer implements Deserializer {
        private final ErrorDecoder errorDecoder;

        EmptyBodyDeserializer(ErrorDecoder errorDecoder) {
            this.errorDecoder = errorDecoder;
        }

        @Override
        @SuppressWarnings("NullAway") // empty body is a special case
        public Void deserialize(Response response) {
            // We should not fail if a server that previously returned nothing starts returning a response
            try (Response unused = response) {
                if (errorDecoder.isError(response)) {
                    throw errorDecoder.decode(response);
                }
                return null;
            }
        }

        @Override
        public Optional accepts() {
            return Optional.empty();
        }

        @Override
        public String toString() {
            return "EmptyBodyDeserializer{}";
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy