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