
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 super T, ? extends R> 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 super List> subscriber;
private final AsyncInflater asyncInflater;
public DecompressedBodySubscriber(Flow.Subscriber super List> 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