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 private InputStream inputStream; 032 private int responseCode; 033 private String bodyString; 034 035 /** 036 * Constructs a BoxAPIResponse using an HttpURLConnection. 037 * @param connection a connection that has already sent a request to the API. 038 */ 039 public BoxAPIResponse(HttpURLConnection connection) { 040 this.connection = connection; 041 this.inputStream = null; 042 043 try { 044 this.responseCode = this.connection.getResponseCode(); 045 } catch (IOException e) { 046 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 047 } 048 049 if (!isSuccess(this.responseCode)) { 050 this.logResponse(); 051 throw new BoxAPIException("The API returned an error code: " + this.responseCode, this.responseCode, 052 this.bodyToString()); 053 } 054 055 this.logResponse(); 056 } 057 058 /** 059 * Gets the response code returned by the API. 060 * @return the response code returned by the API. 061 */ 062 public int getResponseCode() { 063 return this.responseCode; 064 } 065 066 /** 067 * Gets the length of this response's body as indicated by the "Content-Length" header. 068 * @return the length of the response's body. 069 */ 070 public long getContentLength() { 071 return this.connection.getContentLengthLong(); 072 } 073 074 /** 075 * Gets an InputStream for reading this response's body. 076 * @return an InputStream for reading the response's body. 077 */ 078 public InputStream getBody() { 079 return this.getBody(null); 080 } 081 082 /** 083 * Gets an InputStream for reading this response's body which will report its read progress to a ProgressListener. 084 * @param listener a listener for monitoring the read progress of the body. 085 * @return an InputStream for reading the response's body. 086 */ 087 public InputStream getBody(ProgressListener listener) { 088 if (this.inputStream == null) { 089 String contentEncoding = this.connection.getContentEncoding(); 090 try { 091 if (listener == null) { 092 this.inputStream = this.connection.getInputStream(); 093 } else { 094 this.inputStream = new ProgressInputStream(this.connection.getInputStream(), listener, 095 this.getContentLength()); 096 } 097 098 if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { 099 this.inputStream = new GZIPInputStream(this.inputStream); 100 } 101 } catch (IOException e) { 102 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 103 } 104 } 105 106 return this.inputStream; 107 } 108 109 /** 110 * Disconnects this response from the server and frees up any network resources. The body of this response can no 111 * longer be read after it has been disconnected. 112 */ 113 public void disconnect() { 114 if (this.inputStream != null) { 115 try { 116 this.inputStream.close(); 117 } catch (IOException e) { 118 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 119 } 120 } 121 } 122 123 @Override 124 public String toString() { 125 Map<String, List<String>> headers = this.connection.getHeaderFields(); 126 StringBuilder builder = new StringBuilder(); 127 builder.append("Response"); 128 builder.append(System.lineSeparator()); 129 builder.append(this.connection.getRequestMethod()); 130 builder.append(' '); 131 builder.append(this.connection.getURL().toString()); 132 builder.append(System.lineSeparator()); 133 builder.append(headers.get(null).get(0)); 134 builder.append(System.lineSeparator()); 135 136 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 137 String key = entry.getKey(); 138 if (key == null) { 139 continue; 140 } 141 142 List<String> nonEmptyValues = new ArrayList<String>(); 143 for (String value : entry.getValue()) { 144 if (value != null && value.trim().length() != 0) { 145 nonEmptyValues.add(value); 146 } 147 } 148 149 if (nonEmptyValues.size() == 0) { 150 continue; 151 } 152 153 builder.append(key); 154 builder.append(": "); 155 for (String value : nonEmptyValues) { 156 builder.append(value); 157 builder.append(", "); 158 } 159 160 builder.delete(builder.length() - 2, builder.length()); 161 builder.append(System.lineSeparator()); 162 } 163 164 String bodyString = this.bodyToString(); 165 if (bodyString != null && bodyString != "") { 166 builder.append(System.lineSeparator()); 167 builder.append(bodyString); 168 } 169 170 return builder.toString().trim(); 171 } 172 173 /** 174 * Returns a string representation of this response's body. This method is used when logging this response's body. 175 * By default, it returns an empty string (to avoid accidentally logging binary data) unless the response contained 176 * an error message. 177 * @return a string representation of this response's body. 178 */ 179 protected String bodyToString() { 180 if (this.bodyString == null && !isSuccess(this.responseCode)) { 181 this.bodyString = readErrorStream(this.connection.getErrorStream()); 182 } 183 184 return this.bodyString; 185 } 186 187 private void logResponse() { 188 if (LOGGER.isLoggable(Level.FINE)) { 189 LOGGER.log(Level.FINE, this.toString()); 190 } 191 } 192 193 private static boolean isSuccess(int responseCode) { 194 return responseCode >= 200 && responseCode < 300; 195 } 196 197 private static String readErrorStream(InputStream stream) { 198 if (stream == null) { 199 return null; 200 } 201 202 InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); 203 StringBuilder builder = new StringBuilder(); 204 char[] buffer = new char[BUFFER_SIZE]; 205 206 try { 207 int read = reader.read(buffer, 0, BUFFER_SIZE); 208 while (read != -1) { 209 builder.append(buffer, 0, read); 210 read = reader.read(buffer, 0, BUFFER_SIZE); 211 } 212 213 reader.close(); 214 } catch (IOException e) { 215 return null; 216 } 217 218 return builder.toString(); 219 } 220}