
net.dongliu.cute.http.ResponseContext Maven / Gradle / Ivy
package net.dongliu.cute.http;
import net.dongliu.commons.io.InputStreams;
import net.dongliu.commons.io.Readers;
import net.dongliu.commons.reflect.TypeInfer;
import net.dongliu.cute.http.exception.IllegalStatusCodeException;
import net.dongliu.cute.http.exception.JsonMarshallerNotFoundException;
import net.dongliu.cute.http.json.JsonMarshaller;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.IntPredicate;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
/**
* Raw blocking http response. The http headers already received, the http body not consumed, can be get as InputStream.
* It you do not consume http response body, with {@link #readToString()}, {@link #readToBytes()},
* {@link #writeTo(Path)} etc.., you need to close this raw response manually.
*
* This class is not thread-safe.
*
* @author Liu Dong
*/
public class ResponseContext implements AutoCloseable {
private final RawResponse resp;
// user specified charset
private boolean decompressBody = true;
@Nullable
private final JsonMarshaller jsonMarshaller;
ResponseContext(RawResponse resp,
@Nullable JsonMarshaller jsonMarshaller) {
this.resp = requireNonNull(resp);
this.jsonMarshaller = jsonMarshaller;
}
/**
* If decompress http response body automatically. Default True.
*/
public ResponseContext decompressBody(boolean decompressBody) {
this.decompressBody = decompressBody;
return this;
}
/**
* Check the response status code. If not pass the predicate, throw {@link IllegalStatusCodeException}
*
* @throws IllegalArgumentException if check failed.
*/
public ResponseContext checkStatusCode(IntPredicate predicate) throws IllegalStatusCodeException {
var info = resp.info();
if (!predicate.test(info.statusCode())) {
throw new IllegalStatusCodeException(info.statusCode());
}
return this;
}
/**
* Handle response body with handler, return a new response with content as handler result.
* HTTPResponseHandler should consume all InputStream data, or connection may close and cannot reuse.
* The response is closed whether this call succeed or failed with exception.
*/
public Response handle(ResponseHandler responseHandler) {
var info = this.resp.info();
InputStream body;
if (decompressBody) {
body = wrapCompressedInput(resp.method(), info.statusCode(), info.headers(), resp.body());
} else {
body = resp.body();
}
var respInfo = new ResponseInfo(info.statusCode(), info.headers());
try (body) {
T result = responseHandler.handle(respInfo, body);
return new Response<>(resp.url(), info.statusCode(), info.headers(), result);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Handle response content as reader. The reader passed to handler would be closed when handler finished or error occurred.
*
* @param charset the set used to decode the response stream
* @param handler the handler
* @param the response body type to convert to
*/
public Response handleAsReader(ResponseHandler handler, Charset charset) {
return handle((info, body) -> {
try (Reader reader = new InputStreamReader(body, charset)) {
return handler.handle(info, reader);
}
});
}
/**
* Handle response content as reader.
* This method will try to get response charset from header, if not set, will use UTF8.
* The reader passed to handler would be closed when handler finished or error occurred.
*
* @param handler the handler
* @param the response body type to convert to
*/
public Response handleAsReader(ResponseHandler handler) {
var info = resp.info();
return handleAsReader(handler, info.getCharset().orElse(UTF_8));
}
/**
* Wrap response input stream if it is compressed
*/
private InputStream wrapCompressedInput(Method method, int status, Headers headers, InputStream input) {
// if has no body, some server still set content-encoding header,
// GZIPInputStream wrap empty input stream will cause exception. we should check this
if (noBody(method, status)) {
return input;
}
var contentEncoding = headers.getHeader(HeaderNames.CONTENT_ENCODING).orElse("");
//we should remove the content-encoding header here?
switch (contentEncoding) {
case "gzip":
try {
return new GZIPInputStream(input);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
case "deflate":
// Note: deflate implements may or may not wrap in zlib due to rfc confusing.
// here deal with deflate without zlib header
return new InflaterInputStream(input, new Inflater(true));
case "identity":
default:
return input;
}
}
private boolean noBody(Method method, int status) {
return method.equals(Method.HEAD)
|| (status >= 100 && status < 200)
|| status == StatusCodes.NOT_MODIFIED || status == StatusCodes.NO_CONTENT;
}
/**
* Convert to response, with body as text.
* This method will try to get response charset from header, if not set, will use UTF8.
*/
public Response readToString() {
return handleAsReader((info, body) -> Readers.readAll(body));
}
/**
* Convert to response, with body as text.
*
* @param charset the charset to decode response body
*/
public Response readToString(Charset charset) {
return handleAsReader((info, body) -> Readers.readAll(body), charset);
}
/**
* Convert to response, with body as byte array
*/
public Response readToBytes() {
return handle((info, body) -> body.readAllBytes());
}
/**
* Unmarshal response body as json.
* This method will try to get response charset from header, if not set, will use UTF8.
*
* @param The json value type
*/
public Response decodeJson(Class type) {
requireNonNull(type);
if (jsonMarshaller == null) {
throw new JsonMarshallerNotFoundException();
}
return handleAsReader((info, body) -> jsonMarshaller.unmarshal(body, type));
}
/**
* Unmarshal response body as json.
* This method will try to get response charset from header, if not set, will use UTF8.
*
* @param typeInfer for getting actual generic type
* @param The json value type
*/
public Response decodeJson(TypeInfer typeInfer) {
requireNonNull(typeInfer);
if (jsonMarshaller == null) {
throw new JsonMarshallerNotFoundException();
}
return handleAsReader((info, body) -> jsonMarshaller.unmarshal(body, typeInfer.getType()));
}
/**
* Unmarshal response body as json
*
* @param The json value type
* @param charset the charset to decode response body
*/
public Response decodeJson(Class type, Charset charset) {
requireNonNull(type);
requireNonNull(charset);
if (jsonMarshaller == null) {
throw new JsonMarshallerNotFoundException();
}
return handleAsReader((info, body) -> jsonMarshaller.unmarshal(body, type), charset);
}
/**
* Unmarshal response body as json
*
* @param typeInfer for getting actual generic type
* @param charset the charset to decode response body
* @param The json value type
*/
public Response decodeJson(TypeInfer typeInfer, Charset charset) {
requireNonNull(typeInfer);
requireNonNull(charset);
if (jsonMarshaller == null) {
throw new JsonMarshallerNotFoundException();
}
return handleAsReader((info, body) -> jsonMarshaller.unmarshal(body, typeInfer.getType()), charset);
}
/**
* Write response body to file
*/
public Response writeTo(Path path) {
return handle((info, body) -> {
try (var out = Files.newOutputStream(path)) {
body.transferTo(out);
}
return null;
});
}
/**
* Write response body to OutputStream. OutputStream will not be closed.
*/
public Response writeTo(OutputStream out) {
return handle((info, body) -> {
body.transferTo(out);
return null;
});
}
/**
* Write response body to Writer, use charset detected from response header to decode response body.
* If charset in header not set, will use utf-8
* The Writer will be leaved unclosed when finished or exception occurred.
*/
public Response writeTo(Writer writer) {
requireNonNull(writer);
return handleAsReader((info, body) -> {
Readers.transferTo(body, writer);
return null;
});
}
/**
* Write response body to Writer.
* The Writer will be leaved unclosed when finished or exception occurred.
*
* @param charset the charset to decode response body
*/
public Response writeTo(Writer writer, Charset charset) {
requireNonNull(writer);
requireNonNull(charset);
return handleAsReader((info, body) -> {
Readers.transferTo(body, writer);
return null;
});
}
/**
* Consume and discard this response body.
*/
public Response discard() {
return handle((info, body) -> {
InputStreams.discardAll(body);
return null;
});
}
@Override
public void close() {
discard();
}
}