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.*;
import java.util.zip.GZIPInputStream;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
/**
* Response information supplied when a response is received and before the body is processed.
* <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;
/**
* 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);
}
/**
* 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}.
*
* @return the response body
* @throws IOException
* if response stream is null or an I/O Exception occurs.
*/
@Nonnull
public abstract InputStream bodyStream() throws IOException;
/**
* Gets the {@link GitHubConnectorRequest} for this response.
*
* @return the {@link GitHubConnectorRequest} 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;
}
/**
* 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
*/
public abstract static class ByteArrayResponse extends GitHubConnectorResponse {
private boolean inputStreamRead = false;
private byte[] inputBytes = null;
private boolean isClosed = false;
/**
* 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);
}
/**
* {@inheritDoc}
*/
@Override
@Nonnull
public InputStream bodyStream() throws IOException {
if (isClosed) {
throw new IOException("Response is closed");
}
synchronized (this) {
if (!inputStreamRead) {
InputStream rawStream = rawBodyStream();
try (InputStream stream = wrapStream(rawStream)) {
if (stream != null) {
inputBytes = IOUtils.toByteArray(stream);
}
}
inputStreamRead = true;
}
}
if (inputBytes == null) {
throw new IOException("Response body missing, stream null");
}
return new ByteArrayInputStream(inputBytes);
}
/**
* Get the raw implementation specific body stream for this response.
*
* This method will only be called once to completion. If an exception is thrown, it may be called multiple
* times.
*
* @return the stream for the raw response
* @throws IOException
* if an I/O Exception occurs.
*/
@CheckForNull
protected abstract InputStream rawBodyStream() throws IOException;
@Override
public void close() throws IOException {
isClosed = true;
this.inputBytes = null;
}
}
}