GitHubConnectorResponse.java
package org.kohsuke.github.connector;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.zip.GZIPInputStream;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import static java.net.HttpURLConnection.HTTP_OK;
/**
* Response information supplied when a response is received and before the body is processed.
* <p>
* During a request to GitHub, {@link GitHubConnector#send(GitHubConnectorRequest)} returns a
* {@link GitHubConnectorResponse}. This is processed to create a GitHubResponse.
* <p>
* Instances of this class are closed once the response is done being processed. This means that {@link #bodyStream()}
* will not be readable after a call is completed.
*
* {@link #statusCode()}, {@link #allHeaders()}, and {@link #request()} will still be readable but it is recommended
* that consumers copy any information they need rather than retaining a reference to {@link GitHubConnectorResponse}.
*
* @author Liam Newman
*/
public abstract class GitHubConnectorResponse implements Closeable {
private static final Comparator<String> nullableCaseInsensitiveComparator = Comparator
.nullsFirst(String.CASE_INSENSITIVE_ORDER);
private final int statusCode;
@Nonnull
private final GitHubConnectorRequest request;
@Nonnull
private final Map<String, List<String>> headers;
private boolean bodyStreamCalled = false;
private InputStream bodyStream = null;
private byte[] bodyBytes = null;
private boolean isClosed = false;
private boolean isBodyStreamRereadable;
/**
* GitHubConnectorResponse constructor
*
* @param request
* the request
* @param statusCode
* the status code
* @param headers
* the headers
*/
protected GitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
int statusCode,
@Nonnull Map<String, List<String>> headers) {
this.request = request;
this.statusCode = statusCode;
// Response header field names must be case-insensitive.
TreeMap<String, List<String>> caseInsensitiveMap = new TreeMap<>(nullableCaseInsensitiveComparator);
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
caseInsensitiveMap.put(entry.getKey(), Collections.unmodifiableList(new ArrayList<>(entry.getValue())));
}
this.headers = Collections.unmodifiableMap(caseInsensitiveMap);
this.isBodyStreamRereadable = false;
}
/**
* Gets the value of a header field for this response.
*
* @param name
* the name of the header field.
* @return the value of the header field, or {@code null} if the header isn't set.
*/
@CheckForNull
public String header(String name) {
String result = null;
if (headers.containsKey(name)) {
result = headers.get(name).get(0);
}
return result;
}
/**
* The response body as an {@link InputStream}.
*
* When {@link #isBodyStreamRereadable} is false, {@link #bodyStream()} can only be called once and the returned
* stream should be assumed to be read-once and not resetable. This is the default behavior for HTTP_OK responses
* and significantly reduces memory usage.
*
* When {@link #isBodyStreamRereadable} is true, {@link #bodyStream()} can be called be called multiple times. The
* full stream data is read into a byte array during the first call. Each call returns a new stream backed by the
* same byte array. This uses more memory, but is required to enable rereading the body stream during trace logging,
* debugging, and error responses.
*
* @return the response body
* @throws IOException
* if response stream is null or an I/O Exception occurs.
*/
@Nonnull
public InputStream bodyStream() throws IOException {
synchronized (this) {
if (isClosed) {
throw new IOException("Response is closed");
}
if (bodyStreamCalled) {
if (!isBodyStreamRereadable()) {
throw new IOException("Response body not rereadable");
}
} else {
bodyStream = wrapStream(rawBodyStream());
bodyStreamCalled = true;
}
if (bodyStream == null) {
throw new IOException("Response body missing, stream null");
} else if (!isBodyStreamRereadable()) {
return bodyStream;
}
// Load rereadable byte array
if (bodyBytes == null) {
bodyBytes = IOUtils.toByteArray(bodyStream);
// Close the raw body stream after successfully reading
IOUtils.closeQuietly(bodyStream);
}
return new ByteArrayInputStream(bodyBytes);
}
}
/**
* Get the raw implementation specific body stream for this response.
*
* This method will only be called once to completion. If an exception is thrown by this method, it may be called
* multiple times.
*
* The stream returned from this method will be closed when the response is closed or sooner. Inheriting classes do
* not need to close it.
*
* @return the stream for the raw response
* @throws IOException
* if an I/O Exception occurs.
*/
@CheckForNull
protected abstract InputStream rawBodyStream() throws IOException;
/**
* Gets the {@link GitHubConnector} for this response.
*
* @return the {@link GitHubConnector} for this response.
*/
@Nonnull
public GitHubConnectorRequest request() {
return request;
}
/**
* The status code for this response.
*
* @return the status code for this response.
*/
public int statusCode() {
return statusCode;
}
/**
* The headers for this response.
*
* @return the headers for this response.
*/
@Nonnull
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Unmodifiable map of unmodifiable lists")
public Map<String, List<String>> allHeaders() {
return headers;
}
/**
* The body stream rereadable state.
*
* Body stream defaults to read once for HTTP_OK responses (to reduce memory usage). For non-HTTP_OK responses, body
* stream is switched to rereadable (in-memory byte array) for error processing.
*
* Calling {@link #setBodyStreamRereadable()} will force {@link #isBodyStreamRereadable} to be true for this
* response regardless of {@link #statusCode} value.
*
* @return true when body stream is rereadable.
*/
public boolean isBodyStreamRereadable() {
synchronized (this) {
return isBodyStreamRereadable || statusCode != HTTP_OK;
}
}
/**
* Force body stream to rereadable regardless of status code.
*
* Calling {@link #setBodyStreamRereadable()} will force {@link #isBodyStreamRereadable} to be true for this
* response regardless of {@link #statusCode} value.
*
* This is required to support body value logging during low-level tracing but should be avoided in general since it
* consumes significantly more memory.
*
* Will throw runtime exception if a non-rereadable body stream has already been returned from
* {@link #bodyStream()}.
*/
public void setBodyStreamRereadable() {
synchronized (this) {
if (bodyStreamCalled && !isBodyStreamRereadable()) {
throw new RuntimeException("bodyStream() already called in read-once mode");
}
isBodyStreamRereadable = true;
}
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException {
synchronized (this) {
IOUtils.closeQuietly(bodyStream);
isClosed = true;
this.bodyBytes = null;
}
}
/**
* Handles wrapping the body stream if indicated by the "Content-Encoding" header.
*
* @param stream
* the stream to possibly wrap
* @return an input stream potentially wrapped to decode gzip input
* @throws IOException
* if an I/O Exception occurs.
*/
protected InputStream wrapStream(InputStream stream) throws IOException {
String encoding = header("Content-Encoding");
if (encoding == null || stream == null)
return stream;
if (encoding.equals("gzip"))
return new GZIPInputStream(stream);
throw new UnsupportedOperationException("Unexpected Content-Encoding: " + encoding);
}
/**
* Parse a header value as a signed decimal integer.
*
* @param name
* the header field to parse
* @return integer value of the header field
* @throws NumberFormatException
* if the header is missing or does not contain a parsable integer.
*/
public final int parseInt(String name) throws NumberFormatException {
try {
String headerValue = header(name);
return Integer.parseInt(headerValue);
} catch (NumberFormatException e) {
throw new NumberFormatException(name + ": " + e.getMessage());
}
}
/**
* A ByteArrayResponse class
*
* @deprecated Inherit directly from {@link GitHubConnectorResponse}.
*/
@Deprecated
public abstract static class ByteArrayResponse extends GitHubConnectorResponse {
/**
* Constructor for ByteArray Response
*
* @param request
* the request
* @param statusCode
* the status code
* @param headers
* the headers
*/
protected ByteArrayResponse(@Nonnull GitHubConnectorRequest request,
int statusCode,
@Nonnull Map<String, List<String>> headers) {
super(request, statusCode, headers);
}
}
}