001package com.box.sdk; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.io.InputStreamReader; 006import java.net.HttpURLConnection; 007import java.nio.charset.StandardCharsets; 008import java.util.ArrayList; 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(BoxFolder.class.getName()); 028 private static final int BUFFER_SIZE = 8192; 029 030 private final HttpURLConnection connection; 031 032 private int responseCode; 033 private String bodyString; 034 035 /** 036 * The raw InputStream is the stream returned directly from HttpURLConnection.getInputStream(). We need to keep 037 * track of this stream in case we need to access it after wrapping it inside another stream. 038 */ 039 private InputStream rawInputStream; 040 041 /** 042 * The regular InputStream is the stream that will be returned by getBody(). This stream might be a GZIPInputStream 043 * or a ProgressInputStream (or both) that wrap the raw InputStream. 044 */ 045 private InputStream inputStream; 046 047 /** 048 * Constructs an empty BoxAPIResponse without an associated HttpURLConnection. 049 */ 050 public BoxAPIResponse() { 051 this.connection = null; 052 } 053 054 /** 055 * Constructs a BoxAPIResponse using an HttpURLConnection. 056 * @param connection a connection that has already sent a request to the API. 057 */ 058 public BoxAPIResponse(HttpURLConnection connection) { 059 this.connection = connection; 060 this.inputStream = null; 061 062 try { 063 this.responseCode = this.connection.getResponseCode(); 064 } catch (IOException e) { 065 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 066 } 067 068 if (!isSuccess(this.responseCode)) { 069 this.logResponse(); 070 throw new BoxAPIException("The API returned an error code: " + this.responseCode, this.responseCode, 071 this.bodyToString()); 072 } 073 074 this.logResponse(); 075 } 076 077 /** 078 * Gets the response code returned by the API. 079 * @return the response code returned by the API. 080 */ 081 public int getResponseCode() { 082 return this.responseCode; 083 } 084 085 /** 086 * Gets the length of this response's body as indicated by the "Content-Length" header. 087 * @return the length of the response's body. 088 */ 089 public long getContentLength() { 090 return this.connection.getContentLengthLong(); 091 } 092 093 /** 094 * Gets an InputStream for reading this response's body. 095 * @return an InputStream for reading the response's body. 096 */ 097 public InputStream getBody() { 098 return this.getBody(null); 099 } 100 101 /** 102 * Gets an InputStream for reading this response's body which will report its read progress to a ProgressListener. 103 * @param listener a listener for monitoring the read progress of the body. 104 * @return an InputStream for reading the response's body. 105 */ 106 public InputStream getBody(ProgressListener listener) { 107 if (this.inputStream == null) { 108 String contentEncoding = this.connection.getContentEncoding(); 109 try { 110 if (this.rawInputStream == null) { 111 this.rawInputStream = this.connection.getInputStream(); 112 } 113 114 if (listener == null) { 115 this.inputStream = this.rawInputStream; 116 } else { 117 this.inputStream = new ProgressInputStream(this.rawInputStream, listener, 118 this.getContentLength()); 119 } 120 121 if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { 122 this.inputStream = new GZIPInputStream(this.inputStream); 123 } 124 } catch (IOException e) { 125 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 126 } 127 } 128 129 return this.inputStream; 130 } 131 132 /** 133 * Disconnects this response from the server and frees up any network resources. The body of this response can no 134 * longer be read after it has been disconnected. 135 */ 136 public void disconnect() { 137 try { 138 if (this.rawInputStream == null) { 139 this.rawInputStream = this.connection.getInputStream(); 140 } 141 142 // We need to manually read from the raw input stream in case there are any remaining bytes. There's a bug 143 // where a wrapping GZIPInputStream may not read to the end of a chunked response, causing Java to not 144 // return the connection to the connection pool. 145 byte[] buffer = new byte[BUFFER_SIZE]; 146 int n = this.rawInputStream.read(buffer); 147 while (n != -1) { 148 n = this.rawInputStream.read(buffer); 149 } 150 this.rawInputStream.close(); 151 152 if (this.inputStream != null) { 153 this.inputStream.close(); 154 } 155 } catch (IOException e) { 156 throw new BoxAPIException("Couldn't finish closing the connection to the Box API due to a network error or " 157 + "because the stream was already closed.", e); 158 } 159 } 160 161 @Override 162 public String toString() { 163 Map<String, List<String>> headers = this.connection.getHeaderFields(); 164 StringBuilder builder = new StringBuilder(); 165 builder.append("Response"); 166 builder.append(System.lineSeparator()); 167 builder.append(this.connection.getRequestMethod()); 168 builder.append(' '); 169 builder.append(this.connection.getURL().toString()); 170 builder.append(System.lineSeparator()); 171 builder.append(headers.get(null).get(0)); 172 builder.append(System.lineSeparator()); 173 174 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 175 String key = entry.getKey(); 176 if (key == null) { 177 continue; 178 } 179 180 List<String> nonEmptyValues = new ArrayList<String>(); 181 for (String value : entry.getValue()) { 182 if (value != null && value.trim().length() != 0) { 183 nonEmptyValues.add(value); 184 } 185 } 186 187 if (nonEmptyValues.size() == 0) { 188 continue; 189 } 190 191 builder.append(key); 192 builder.append(": "); 193 for (String value : nonEmptyValues) { 194 builder.append(value); 195 builder.append(", "); 196 } 197 198 builder.delete(builder.length() - 2, builder.length()); 199 builder.append(System.lineSeparator()); 200 } 201 202 String bodyString = this.bodyToString(); 203 if (bodyString != null && bodyString != "") { 204 builder.append(System.lineSeparator()); 205 builder.append(bodyString); 206 } 207 208 return builder.toString().trim(); 209 } 210 211 /** 212 * Returns a string representation of this response's body. This method is used when logging this response's body. 213 * By default, it returns an empty string (to avoid accidentally logging binary data) unless the response contained 214 * an error message. 215 * @return a string representation of this response's body. 216 */ 217 protected String bodyToString() { 218 if (this.bodyString == null && !isSuccess(this.responseCode)) { 219 this.bodyString = readErrorStream(this.connection.getErrorStream()); 220 } 221 222 return this.bodyString; 223 } 224 225 private void logResponse() { 226 if (LOGGER.isLoggable(Level.FINE)) { 227 LOGGER.log(Level.FINE, this.toString()); 228 } 229 } 230 231 private static boolean isSuccess(int responseCode) { 232 return responseCode >= 200 && responseCode < 300; 233 } 234 235 private static String readErrorStream(InputStream stream) { 236 if (stream == null) { 237 return null; 238 } 239 240 InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); 241 StringBuilder builder = new StringBuilder(); 242 char[] buffer = new char[BUFFER_SIZE]; 243 244 try { 245 int read = reader.read(buffer, 0, BUFFER_SIZE); 246 while (read != -1) { 247 builder.append(buffer, 0, read); 248 read = reader.read(buffer, 0, BUFFER_SIZE); 249 } 250 251 reader.close(); 252 } catch (IOException e) { 253 return null; 254 } 255 256 return builder.toString(); 257 } 258}