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

net.dongliu.cute.http.AsyncResponseContext Maven / Gradle / Ivy

The newest version!
package net.dongliu.cute.http;

import net.dongliu.commons.reflect.TypeInfer;
import net.dongliu.cute.http.exception.JsonMarshallerNotFoundException;
import net.dongliu.cute.http.internal.AsyncInflater;
import net.dongliu.cute.http.json.JsonMarshaller;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.io.IOException;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.lang.reflect.Type;
import java.net.http.HttpResponse.BodySubscribers;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Publisher;
import java.util.function.Function;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

/**
 * Async Http Response.
 */
public class AsyncResponseContext {
    private final CompletableFuture>>> responseFuture;
    @Nullable
    private final JsonMarshaller jsonMarshaller;
    private boolean autoDecompress = true;

    AsyncResponseContext(CompletableFuture>>> responseFuture,
                         @Nullable JsonMarshaller jsonMarshaller) {
        this.responseFuture = responseFuture;
        this.jsonMarshaller = jsonMarshaller;
    }


    /**
     * If decompress response body automatically. Default true
     */
    public AsyncResponseContext autoDecompress(boolean autoDecompress) {
        this.autoDecompress = autoDecompress;
        return this;
    }

    /**
     * Handle the response, return a response future. If any exceptions were thrown during call handler, will return a
     * exceptional Future.
     *
     * @param subscriberProvider provide a BodySubscriber to handle the body
     * @param       return value type
     */
    public  CompletableFuture> handle(BodySubscriberProvider subscriberProvider) {
        requireNonNull(subscriberProvider);
        return responseFuture.thenCompose(resp -> {
            var respInfo = resp.info();
            var bodyPublisher = resp.body();
            if (autoDecompress) {
                bodyPublisher = wrapCompressedPublisher(resp.method(), respInfo.statusCode(), respInfo.headers(), resp.body());
            }

            var subscriber = subscriberProvider.getBodySubscriber(respInfo);
            bodyPublisher.subscribe(subscriber);
            var bodyFuture = subscriber.getBody();

            return bodyFuture.thenApply(body -> new Response<>(
                    resp.url(),
                    respInfo.statusCode(),
                    respInfo.headers(),
                    body
            ));
        });
    }

    /**
     * Handle the response using a BodySubscriber, then transformed the result by the function mapper.
     * If any exceptions were thrown during call handler, will return a exceptional Future.
     *
     * @param subscriberProvider provider the BodySubscriber to handle the response
     * @param mapper             the function to convert the body get by the BodySubscriber
     * @param                 return value type
     */
    public  CompletableFuture> handle(BodySubscriberProvider subscriberProvider,
                                                        Function mapper) {
        requireNonNull(subscriberProvider);

        requireNonNull(mapper);
        return handle(respInfo -> BodySubscribers.mapping(subscriberProvider.getBodySubscriber(respInfo), mapper));
    }

    /**
     * Collect the response body as String.
     *
     * @param charset the charset used to decode response body.
     * @return the response Future
     */
    public CompletableFuture> readToString(Charset charset) {
        requireNonNull(charset);
        return handle(info -> BodySubscribers.ofString(charset));
    }

    /**
     * Collect the response body as String.
     * This method would get charset from response headers. If not set, would use UTF-8.
     *
     * @return the response Future
     */
    public CompletableFuture> readToString() {
        return handle(info -> BodySubscribers.ofString(info.getCharset().orElse(UTF_8)));
    }

    /**
     * Collect the response body to byte array.
     *
     * @return the response Future
     */
    public CompletableFuture> readToBytes() {
        return handle(info -> BodySubscribers.ofByteArray());
    }

    /**
     * Unmarshal response body as json.
     * This method would get charset from response headers. If not set, would use UTF-8.
     *
     * @param  The json value type
     */
    public  CompletableFuture> decodeJson(Class type) {
        requireNonNull(type);
        if (jsonMarshaller == null) {
            throw new JsonMarshallerNotFoundException();
        }
        return handle(info -> BodySubscribers.ofString(info.getCharset().orElse(UTF_8)),
                s -> decodeJson(s, type));
    }

    /**
     * Unmarshal response body as json.
     * This method would get charset from response headers. If not set, would use UTF-8.
     *
     * @param typeInfer for getting actual generic type
     * @param        The json value type
     */
    public  CompletableFuture> decodeJson(TypeInfer typeInfer) {
        requireNonNull(typeInfer);
        if (jsonMarshaller == null) {
            throw new JsonMarshallerNotFoundException();
        }
        return handle(info -> BodySubscribers.ofString(info.getCharset().orElse(UTF_8)),
                s -> decodeJson(s, typeInfer.getType()));
    }

    /**
     * Unmarshal response body as json.
     *
     * @param charset the charset used to decode response body.
     * @param      The json value type
     */
    public  CompletableFuture> decodeJson(Class type, Charset charset) {
        requireNonNull(type);
        if (jsonMarshaller == null) {
            throw new JsonMarshallerNotFoundException();
        }
        return handle(info -> BodySubscribers.ofString(charset), s -> decodeJson(s, type));
    }

    /**
     * Unmarshal response body as json
     *
     * @param charset   the charset used to decode response body.
     * @param typeInfer for getting actual generic type
     * @param        The json value type
     */
    public  CompletableFuture> decodeJson(TypeInfer typeInfer, Charset charset) {
        requireNonNull(typeInfer);
        if (jsonMarshaller == null) {
            throw new JsonMarshallerNotFoundException();
        }
        return handle(info -> BodySubscribers.ofString(charset), s -> decodeJson(s, typeInfer.getType()));
    }

    private  T decodeJson(String json, Type type) {
        if (jsonMarshaller == null) {
            throw new JsonMarshallerNotFoundException();
        }
        try (var reader = new StringReader(json)) {
            try {
                return jsonMarshaller.unmarshal(reader, type);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    }

    /**
     * Discard all response body.
     *
     * @return the response Future
     */
    public CompletableFuture> discard() {
        return handle(info -> BodySubscribers.discarding());
    }

    /**
     * Write response body to file.
     *
     * @param path the file path
     * @return the response future
     */
    public CompletableFuture> writeTo(Path path) {
        return handle(info -> BodySubscribers.ofFile(path));
    }

    private Publisher> wrapCompressedPublisher(Method method, int status, Headers headers,
                                                                Publisher> publisher) {
        if (responseHasNoBody(method, status)) {
            return publisher;
        }
        var contentEncoding = headers.getHeader(HeaderNames.CONTENT_ENCODING).orElse("").trim();
        switch (contentEncoding) {
            case "gzip":
                return subscriber -> publisher.subscribe(new DecompressedBodySubscriber(subscriber, AsyncInflater.GZIP));
            case "deflate":
                // Note: deflate implements may or may not wrap in zlib due to rfc confusing.
                // here deal with deflate without zlib header
                return subscriber -> publisher.subscribe(new DecompressedBodySubscriber(subscriber, AsyncInflater.ZLIB));
            case "identity":
            default:
                return publisher;
        }
    }

    private boolean responseHasNoBody(Method method, int status) {
        return method.equals(Method.HEAD)
                || (status >= 100 && status < 200)
                || status == StatusCodes.NOT_MODIFIED || status == StatusCodes.NO_CONTENT;
    }

    /**
     * Delegated Body Subscriber which decompress response content.
     */
    private static class DecompressedBodySubscriber implements Flow.Subscriber> {

        private final Flow.Subscriber> subscriber;
        private final AsyncInflater asyncInflater;

        public DecompressedBodySubscriber(Flow.Subscriber> subscriber, int wrapper) {
            this.subscriber = subscriber;
            this.asyncInflater = new AsyncInflater(wrapper);
        }

        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            subscriber.onSubscribe(subscription);
        }

        @Override
        public void onNext(List item) {
            var buffers = new ArrayList();
            for (var in : item) {
                asyncInflater.decode(in, buffers::add);
            }
            subscriber.onNext(buffers);
        }

        @Override
        public void onError(Throwable throwable) {
            asyncInflater.onFinish();
            subscriber.onError(throwable);
        }

        @Override
        public void onComplete() {
            asyncInflater.onFinish();
            subscriber.onComplete();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy