com.rt.storage.api.client.http.HttpResponse Maven / Gradle / Ivy
package com.rt.storage.api.client.http;
import com.rt.storage.api.client.util.Charsets;
import com.rt.storage.api.client.util.IOUtils;
import com.rt.storage.api.client.util.LoggingInputStream;
import com.rt.storage.api.client.util.Preconditions;
import com.rt.storage.api.client.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
/**
* HTTP response.
*
* Callers should call {@link #disconnect} when the HTTP response object is no longer needed.
* However, {@link #disconnect} does not have to be called if the response stream is properly
* closed. Example usage:
*
*
* HttpResponse response = request.execute();
* try {
* // process the HTTP response object
* } finally {
* response.disconnect();
* }
*
*
* Implementation is not thread-safe.
*
* @since 1.0
* @author Yaniv Inbar
*/
public final class HttpResponse {
/** HTTP response content or {@code null} before {@link #getContent()}. */
private InputStream content;
/** Content encoding or {@code null}. */
private final String contentEncoding;
/** Content type or {@code null} for none. */
private final String contentType;
/** Parsed content-type/media type or {@code null} if content-type is null. */
private final HttpMediaType mediaType;
/** Low-level HTTP response. */
LowLevelHttpResponse response;
/** Status code. */
private final int statusCode;
/** Status message or {@code null}. */
private final String statusMessage;
/** HTTP request. */
private final HttpRequest request;
/** Whether {@link #getContent()} should return raw input stream. */
private final boolean returnRawInputStream;
/** Content encoding for GZip */
private static final String CONTENT_ENCODING_GZIP = "gzip";
/** Content encoding for GZip (legacy) */
private static final String CONTENT_ENCODING_XGZIP = "x-gzip";
/**
* Determines the limit to the content size that will be logged during {@link #getContent()}.
*
*
Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
*
If the content size is greater than this limit then it will not be logged.
*
*
Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
*
Defaults to {@link HttpRequest#getContentLoggingLimit()}.
*/
private int contentLoggingLimit;
/**
* Determines whether logging should be enabled on this response.
*
*
Defaults to {@link HttpRequest#isLoggingEnabled()}.
*/
private boolean loggingEnabled;
/** Signals whether the content has been read from the input stream. */
private boolean contentRead;
HttpResponse(HttpRequest request, LowLevelHttpResponse response) throws IOException {
this.request = request;
this.returnRawInputStream = request.getResponseReturnRawInputStream();
contentLoggingLimit = request.getContentLoggingLimit();
loggingEnabled = request.isLoggingEnabled();
this.response = response;
contentEncoding = response.getContentEncoding();
int code = response.getStatusCode();
statusCode = code < 0 ? 0 : code;
String message = response.getReasonPhrase();
statusMessage = message;
Logger logger = HttpTransport.LOGGER;
boolean loggable = loggingEnabled && logger.isLoggable(Level.CONFIG);
StringBuilder logbuf = null;
if (loggable) {
logbuf = new StringBuilder();
logbuf.append("-------------- RESPONSE --------------").append(StringUtils.LINE_SEPARATOR);
String statusLine = response.getStatusLine();
if (statusLine != null) {
logbuf.append(statusLine);
} else {
logbuf.append(statusCode);
if (message != null) {
logbuf.append(' ').append(message);
}
}
logbuf.append(StringUtils.LINE_SEPARATOR);
}
// headers
request.getResponseHeaders().fromHttpResponse(response, loggable ? logbuf : null);
// Retrieve the content-type directly from the headers as response.getContentType() is outdated
// and e.g. not set by BatchUnparsedResponse.FakeLowLevelHttpResponse
String contentType = response.getContentType();
if (contentType == null) {
contentType = request.getResponseHeaders().getContentType();
}
this.contentType = contentType;
this.mediaType = parseMediaType(contentType);
// log from buffer
if (loggable) {
logger.config(logbuf.toString());
}
}
/**
* Returns an {@link HttpMediaType} object parsed from {@link #contentType}, or {@code null} if if
* {@link #contentType} cannot be parsed or {@link #contentType} is {@code null}.
*/
private static HttpMediaType parseMediaType(String contentType) {
if (contentType == null) {
return null;
}
try {
return new HttpMediaType(contentType);
} catch (IllegalArgumentException e) {
// contentType is invalid and cannot be parsed.
return null;
}
}
/**
* Returns the limit to the content size that will be logged during {@link #getContent()}.
*
*
Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
*
If the content size is greater than this limit then it will not be logged.
*
*
Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
*
Defaults to {@link HttpRequest#getContentLoggingLimit()}.
*
* @since 1.7
*/
public int getContentLoggingLimit() {
return contentLoggingLimit;
}
/**
* Set the limit to the content size that will be logged during {@link #getContent()}.
*
*
Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
*
If the content size is greater than this limit then it will not be logged.
*
*
Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
*
Defaults to {@link HttpRequest#getContentLoggingLimit()}.
*
* @since 1.7
*/
public HttpResponse setContentLoggingLimit(int contentLoggingLimit) {
Preconditions.checkArgument(
contentLoggingLimit >= 0, "The content logging limit must be non-negative.");
this.contentLoggingLimit = contentLoggingLimit;
return this;
}
/**
* Returns whether logging should be enabled on this response.
*
*
Defaults to {@link HttpRequest#isLoggingEnabled()}.
*
* @since 1.9
*/
public boolean isLoggingEnabled() {
return loggingEnabled;
}
/**
* Sets whether logging should be enabled on this response.
*
*
Defaults to {@link HttpRequest#isLoggingEnabled()}.
*
* @since 1.9
*/
public HttpResponse setLoggingEnabled(boolean loggingEnabled) {
this.loggingEnabled = loggingEnabled;
return this;
}
/**
* Returns the content encoding or {@code null} for none.
*
* @since 1.5
*/
public String getContentEncoding() {
return contentEncoding;
}
/**
* Returns the content type or {@code null} for none.
*
* @since 1.5
*/
public String getContentType() {
return contentType;
}
/**
* Returns the parsed Content-Type in form of a {@link HttpMediaType} or {@code null} if no
* content-type was set.
*
* @since 1.10
*/
public HttpMediaType getMediaType() {
return mediaType;
}
/**
* Returns the HTTP response headers.
*
* @since 1.5
*/
public HttpHeaders getHeaders() {
return request.getResponseHeaders();
}
/**
* Returns whether received a successful HTTP status code {@code >= 200 && < 300} (see {@link
* #getStatusCode()}).
*
* @since 1.5
*/
public boolean isSuccessStatusCode() {
return HttpStatusCodes.isSuccess(statusCode);
}
/**
* Returns the HTTP status code or {@code 0} for none.
*
* @since 1.5
*/
public int getStatusCode() {
return statusCode;
}
/**
* Returns the HTTP status message or {@code null} for none.
*
* @since 1.5
*/
public String getStatusMessage() {
return statusMessage;
}
/**
* Returns the HTTP transport.
*
* @since 1.5
*/
public HttpTransport getTransport() {
return request.getTransport();
}
/**
* Returns the HTTP request.
*
* @since 1.5
*/
public HttpRequest getRequest() {
return request;
}
/**
* Returns the content of the HTTP response.
*
*
The result is cached, so subsequent calls will be fast.
*
*
Callers should call {@link InputStream#close} after the returned {@link InputStream} is no
* longer needed. Example usage:
*
*
* InputStream is = response.getContent();
* try {
* // Process the input stream..
* } finally {
* is.close();
* }
*
*
* {@link HttpResponse#disconnect} does not have to be called if the content is closed.
*
* @return input stream content of the HTTP response or {@code null} for none
* @throws IOException I/O exception
*/
public InputStream getContent() throws IOException {
if (!contentRead) {
InputStream lowLevelResponseContent = this.response.getContent();
if (lowLevelResponseContent != null) {
// Flag used to indicate if an exception is thrown before the content is successfully
// processed.
boolean contentProcessed = false;
try {
// gzip encoding (wrap content with GZipInputStream)
if (!returnRawInputStream && this.contentEncoding != null) {
String oontentencoding = this.contentEncoding.trim().toLowerCase(Locale.ENGLISH);
if (CONTENT_ENCODING_GZIP.equals(oontentencoding)
|| CONTENT_ENCODING_XGZIP.equals(oontentencoding)) {
// Wrap the original stream in a ConsumingInputStream before passing it to
// GZIPInputStream. The GZIPInputStream leaves content unconsumed in the original
// stream (it almost always leaves the last chunk unconsumed in chunked responses).
// ConsumingInputStream ensures that any unconsumed bytes are read at close.
// GZIPInputStream.close() --> ConsumingInputStream.close() -->
// exhaust(ConsumingInputStream)
lowLevelResponseContent =
new GZIPInputStream(new ConsumingInputStream(lowLevelResponseContent));
}
}
// logging (wrap content with LoggingInputStream)
Logger logger = HttpTransport.LOGGER;
if (loggingEnabled && logger.isLoggable(Level.CONFIG)) {
lowLevelResponseContent =
new LoggingInputStream(
lowLevelResponseContent, logger, Level.CONFIG, contentLoggingLimit);
}
content = lowLevelResponseContent;
contentProcessed = true;
} catch (EOFException e) {
// this may happen for example on a HEAD request since there no actual response data read
// in GZIPInputStream
} finally {
if (!contentProcessed) {
lowLevelResponseContent.close();
}
}
}
contentRead = true;
}
return content;
}
/**
* Writes the content of the HTTP response into the given destination output stream.
*
*
Sample usage:
*
*
* HttpRequest request = requestFactory.buildGetRequest(
* new GenericUrl("https://www.baidu.com/images/srpr/logo3w.png"));
* OutputStream outputStream = new FileOutputStream(new File("/tmp/logo3w.png"));
* try {
* HttpResponse response = request.execute();
* response.download(outputStream);
* } finally {
* outputStream.close();
* }
*
*
* This method closes the content of the HTTP response from {@link #getContent()}.
*
*
This method does not close the given output stream.
*
* @param outputStream destination output stream
* @throws IOException I/O exception
* @since 1.9
*/
public void download(OutputStream outputStream) throws IOException {
InputStream inputStream = getContent();
IOUtils.copy(inputStream, outputStream);
}
/** Closes the content of the HTTP response from {@link #getContent()}, ignoring any content. */
public void ignore() throws IOException {
InputStream content = getContent();
if (content != null) {
content.close();
}
}
/**
* Close the HTTP response content using {@link #ignore}, and disconnect using {@link
* LowLevelHttpResponse#disconnect()}.
*
* @since 1.4
*/
public void disconnect() throws IOException {
ignore();
response.disconnect();
}
/**
* Parses the content of the HTTP response from {@link #getContent()} and reads it into a data
* class of key/value pairs using the parser returned by {@link HttpRequest#getParser()}.
*
*
Reference: http://tools.ietf.org/html/rfc2616#section-4.3
*
* @return parsed data class or {@code null} for no content
*/
public T parseAs(Class dataClass) throws IOException {
if (!hasMessageBody()) {
return null;
}
return request.getParser().parseAndClose(getContent(), getContentCharset(), dataClass);
}
/**
* Returns whether this response contains a message body as specified in {@href
* http://tools.ietf.org/html/rfc2616#section-4.3}, calling {@link #ignore()} if {@code false}.
*/
private boolean hasMessageBody() throws IOException {
int statusCode = getStatusCode();
if (getRequest().getRequestMethod().equals(HttpMethods.HEAD)
|| statusCode / 100 == 1
|| statusCode == HttpStatusCodes.STATUS_CODE_NO_CONTENT
|| statusCode == HttpStatusCodes.STATUS_CODE_NOT_MODIFIED) {
ignore();
return false;
}
return true;
}
/**
* Parses the content of the HTTP response from {@link #getContent()} and reads it into a data
* type of key/value pairs using the parser returned by {@link HttpRequest#getParser()}.
*
* @return parsed data type instance or {@code null} for no content
* @since 1.10
*/
public Object parseAs(Type dataType) throws IOException {
if (!hasMessageBody()) {
return null;
}
return request.getParser().parseAndClose(getContent(), getContentCharset(), dataType);
}
/**
* Parses the content of the HTTP response from {@link #getContent()} and reads it into a string.
*
* Since this method returns {@code ""} for no content, a simpler check for no content is to
* check if {@link #getContent()} is {@code null}.
*
*
All content is read from the input content stream rather than being limited by the
* Content-Length. For the character set, it follows the specification by parsing the "charset"
* parameter of the Content-Type header or by default {@code "ISO-8859-1"} if the parameter is
* missing.
*
* @return parsed string or {@code ""} for no content
* @throws IOException I/O exception
*/
public String parseAsString() throws IOException {
InputStream content = getContent();
if (content == null) {
return "";
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(content, out);
return out.toString(getContentCharset().name());
}
/**
* Returns the {@link Charset} specified in the Content-Type of this response or the {@code
* "ISO-8859-1"} charset as a default.
*
* @since 1.10
*/
public Charset getContentCharset() {
return mediaType == null || mediaType.getCharsetParameter() == null
? Charsets.ISO_8859_1
: mediaType.getCharsetParameter();
}
}