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}