001package com.box.sdk; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.io.InputStreamReader; 006import java.net.HttpURLConnection; 007import java.util.ArrayList; 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.logging.Level; 012import java.util.logging.Logger; 013import java.util.zip.GZIPInputStream; 014 015/** 016 * Used to read HTTP responses from the Box API. 017 * 018 * <p>All responses from the REST API are read using this class or one of its subclasses. This class wraps {@link 019 * HttpURLConnection} in order to provide a simpler interface that can automatically handle various conditions specific 020 * to Box's API. When a response is contructed, it will throw a {@link BoxAPIException} if the response from the API 021 * was an error. Therefore every BoxAPIResponse instance is guaranteed to represent a successful response.</p> 022 * 023 * <p>This class usually isn't instantiated directly, but is instead returned after calling {@link BoxAPIRequest#send}. 024 * </p> 025 */ 026public class BoxAPIResponse { 027 private static final Logger LOGGER = Logger.getLogger(BoxAPIResponse.class.getName()); 028 private static final int BUFFER_SIZE = 8192; 029 030 private final HttpURLConnection connection; 031 //Batch API Response will have headers in response body 032 private final Map<String, String> headers; 033 034 private int responseCode; 035 private String bodyString; 036 037 /** 038 * The raw InputStream is the stream returned directly from HttpURLConnection.getInputStream(). We need to keep 039 * track of this stream in case we need to access it after wrapping it inside another stream. 040 */ 041 private InputStream rawInputStream; 042 043 /** 044 * The regular InputStream is the stream that will be returned by getBody(). This stream might be a GZIPInputStream 045 * or a ProgressInputStream (or both) that wrap the raw InputStream. 046 */ 047 private InputStream inputStream; 048 049 /** 050 * Constructs an empty BoxAPIResponse without an associated HttpURLConnection. 051 */ 052 public BoxAPIResponse() { 053 this.connection = null; 054 this.headers = null; 055 } 056 057 /** 058 * Constructs a BoxAPIResponse with a http response code and response headers. 059 * @param responseCode http response code 060 * @param headers map of headers 061 */ 062 public BoxAPIResponse(int responseCode, Map<String, String> headers) { 063 this.connection = null; 064 this.responseCode = responseCode; 065 this.headers = headers; 066 } 067 068 /** 069 * Constructs a BoxAPIResponse using an HttpURLConnection. 070 * @param connection a connection that has already sent a request to the API. 071 */ 072 public BoxAPIResponse(HttpURLConnection connection) { 073 this.connection = connection; 074 this.inputStream = null; 075 076 try { 077 this.responseCode = this.connection.getResponseCode(); 078 } catch (IOException e) { 079 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 080 } 081 082 Map<String, String> responseHeaders = new HashMap<String, String>(); 083 for (String headerKey : connection.getHeaderFields().keySet()) { 084 responseHeaders.put(headerKey, connection.getHeaderField(headerKey)); 085 } 086 this.headers = responseHeaders; 087 088 if (!isSuccess(this.responseCode)) { 089 this.logResponse(); 090 throw new BoxAPIResponseException("The API returned an error code", this); 091 } 092 093 this.logResponse(); 094 } 095 096 /** 097 * Gets the response code returned by the API. 098 * @return the response code returned by the API. 099 */ 100 public int getResponseCode() { 101 return this.responseCode; 102 } 103 104 /** 105 * Gets the length of this response's body as indicated by the "Content-Length" header. 106 * @return the length of the response's body. 107 */ 108 public long getContentLength() { 109 return this.connection.getContentLength(); 110 } 111 112 /** 113 * Gets the value of the given header field. 114 * @param fieldName name of the header field. 115 * @return value of the header. 116 */ 117 public String getHeaderField(String fieldName) { 118 // headers map is null for all regular response calls except when made as a batch request 119 if (this.headers == null) { 120 if (this.connection != null) { 121 return this.connection.getHeaderField(fieldName); 122 } else { 123 return null; 124 } 125 } else { 126 return this.headers.get(fieldName); 127 } 128 } 129 130 /** 131 * Gets an InputStream for reading this response's body. 132 * @return an InputStream for reading the response's body. 133 */ 134 public InputStream getBody() { 135 return this.getBody(null); 136 } 137 138 /** 139 * Gets an InputStream for reading this response's body which will report its read progress to a ProgressListener. 140 * @param listener a listener for monitoring the read progress of the body. 141 * @return an InputStream for reading the response's body. 142 */ 143 public InputStream getBody(ProgressListener listener) { 144 if (this.inputStream == null) { 145 String contentEncoding = this.connection.getContentEncoding(); 146 try { 147 if (this.rawInputStream == null) { 148 this.rawInputStream = this.connection.getInputStream(); 149 } 150 151 if (listener == null) { 152 this.inputStream = this.rawInputStream; 153 } else { 154 this.inputStream = new ProgressInputStream(this.rawInputStream, listener, 155 this.getContentLength()); 156 } 157 158 if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { 159 this.inputStream = new GZIPInputStream(this.inputStream); 160 } 161 } catch (IOException e) { 162 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 163 } 164 } 165 166 return this.inputStream; 167 } 168 169 /** 170 * Disconnects this response from the server and frees up any network resources. The body of this response can no 171 * longer be read after it has been disconnected. 172 */ 173 public void disconnect() { 174 if (this.connection == null) { 175 return; 176 } 177 178 try { 179 if (this.rawInputStream == null) { 180 this.rawInputStream = this.connection.getInputStream(); 181 } 182 183 // We need to manually read from the raw input stream in case there are any remaining bytes. There's a bug 184 // where a wrapping GZIPInputStream may not read to the end of a chunked response, causing Java to not 185 // return the connection to the connection pool. 186 byte[] buffer = new byte[BUFFER_SIZE]; 187 int n = this.rawInputStream.read(buffer); 188 while (n != -1) { 189 n = this.rawInputStream.read(buffer); 190 } 191 this.rawInputStream.close(); 192 193 if (this.inputStream != null) { 194 this.inputStream.close(); 195 } 196 } catch (IOException e) { 197 throw new BoxAPIException("Couldn't finish closing the connection to the Box API due to a network error or " 198 + "because the stream was already closed.", e); 199 } 200 } 201 202 /** 203 * 204 * @return A Map containg headers on this Box API Response. 205 */ 206 public Map<String, String> getHeaders() { 207 return this.headers; 208 } 209 210 @Override 211 public String toString() { 212 String lineSeparator = System.getProperty("line.separator"); 213 Map<String, List<String>> headers = this.connection.getHeaderFields(); 214 StringBuilder builder = new StringBuilder(); 215 builder.append("Response"); 216 builder.append(lineSeparator); 217 builder.append(this.connection.getRequestMethod()); 218 builder.append(' '); 219 builder.append(this.connection.getURL().toString()); 220 builder.append(lineSeparator); 221 builder.append(headers.get(null).get(0)); 222 builder.append(lineSeparator); 223 224 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 225 String key = entry.getKey(); 226 if (key == null) { 227 continue; 228 } 229 230 List<String> nonEmptyValues = new ArrayList<String>(); 231 for (String value : entry.getValue()) { 232 if (value != null && value.trim().length() != 0) { 233 nonEmptyValues.add(value); 234 } 235 } 236 237 if (nonEmptyValues.size() == 0) { 238 continue; 239 } 240 241 builder.append(key); 242 builder.append(": "); 243 for (String value : nonEmptyValues) { 244 builder.append(value); 245 builder.append(", "); 246 } 247 248 builder.delete(builder.length() - 2, builder.length()); 249 builder.append(lineSeparator); 250 } 251 252 String bodyString = this.bodyToString(); 253 if (bodyString != null && bodyString != "") { 254 builder.append(lineSeparator); 255 builder.append(bodyString); 256 } 257 258 return builder.toString().trim(); 259 } 260 261 /** 262 * Returns a string representation of this response's body. This method is used when logging this response's body. 263 * By default, it returns an empty string (to avoid accidentally logging binary data) unless the response contained 264 * an error message. 265 * @return a string representation of this response's body. 266 */ 267 protected String bodyToString() { 268 if (this.bodyString == null && !isSuccess(this.responseCode)) { 269 this.bodyString = readErrorStream(this.getErrorStream()); 270 } 271 272 return this.bodyString; 273 } 274 275 /** 276 * Returns the response error stream, handling the case when it contains gzipped data. 277 * @return gzip decoded (if needed) error stream or null 278 */ 279 private InputStream getErrorStream() { 280 InputStream errorStream = this.connection.getErrorStream(); 281 if (errorStream != null) { 282 final String contentEncoding = this.connection.getContentEncoding(); 283 if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { 284 try { 285 errorStream = new GZIPInputStream(errorStream); 286 } catch (IOException e) { 287 // just return the error stream as is 288 } 289 } 290 } 291 292 return errorStream; 293 } 294 295 private void logResponse() { 296 if (LOGGER.isLoggable(Level.FINE)) { 297 LOGGER.log(Level.FINE, this.toString()); 298 } 299 } 300 301 private static boolean isSuccess(int responseCode) { 302 return responseCode >= 200 && responseCode < 300; 303 } 304 305 private static String readErrorStream(InputStream stream) { 306 if (stream == null) { 307 return null; 308 } 309 310 InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); 311 StringBuilder builder = new StringBuilder(); 312 char[] buffer = new char[BUFFER_SIZE]; 313 314 try { 315 int read = reader.read(buffer, 0, BUFFER_SIZE); 316 while (read != -1) { 317 builder.append(buffer, 0, read); 318 read = reader.read(buffer, 0, BUFFER_SIZE); 319 } 320 321 reader.close(); 322 } catch (IOException e) { 323 return null; 324 } 325 326 return builder.toString(); 327 } 328}