• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / box-java-sdk / #2995

pending completion
#2995

Pull #1174

github

web-flow
Merge 242a9da83 into 4d1616ddd
Pull Request #1174: fix: class cast exception when uploading large file

13 of 13 new or added lines in 3 files covered. (100.0%)

7123 of 9994 relevant lines covered (71.27%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.92
/src/main/java/com/box/sdk/BoxAPIResponse.java
1
package com.box.sdk;
2

3
import static com.box.sdk.StandardCharsets.UTF_8;
4
import static com.box.sdk.http.ContentType.APPLICATION_JSON;
5
import static java.lang.String.format;
6

7
import com.eclipsesource.json.Json;
8
import com.eclipsesource.json.ParseException;
9
import java.io.ByteArrayInputStream;
10
import java.io.Closeable;
11
import java.io.IOException;
12
import java.io.InputStream;
13
import java.io.InputStreamReader;
14
import java.net.HttpURLConnection;
15
import java.util.List;
16
import java.util.Map;
17
import java.util.Objects;
18
import java.util.Optional;
19
import java.util.TreeMap;
20
import okhttp3.MediaType;
21
import okhttp3.Response;
22
import okhttp3.ResponseBody;
23

24
/**
25
 * Used to read HTTP responses from the Box API.
26
 *
27
 * <p>
28
 * All responses from the REST API are read using this class or one of its subclasses. This class wraps {@link
29
 * HttpURLConnection} in order to provide a simpler interface that can automatically handle various conditions specific
30
 * to Box's API. When a response is contructed, it will throw a {@link BoxAPIException} if the response from the API
31
 * was an error. Therefore every BoxAPIResponse instance is guaranteed to represent a successful response.
32
 * </p>
33
 *
34
 * <p>
35
 * This class usually isn't instantiated directly, but is instead returned after calling {@link BoxAPIRequest#send}.
36
 * </p>
37
 */
38
public class BoxAPIResponse implements Closeable {
39
    private static final int BUFFER_SIZE = 8192;
40
    private static final BoxLogger LOGGER = BoxLogger.defaultLogger();
1✔
41
    private final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
1✔
42
    private final long contentLength;
43
    private final String contentType;
44
    private final String requestMethod;
45
    private final String requestUrl;
46
    private int responseCode;
47
    private String bodyString;
48

49
    /**
50
     * The raw InputStream is the stream returned directly from HttpURLConnection.getInputStream(). We need to keep
51
     * track of this stream in case we need to access it after wrapping it inside another stream.
52
     */
53
    private InputStream rawInputStream;
54

55
    /**
56
     * The regular InputStream is the stream that will be returned by getBody(). This stream might be a GZIPInputStream
57
     * or a ProgressInputStream (or both) that wrap the raw InputStream.
58
     */
59
    private InputStream inputStream;
60

61
    /**
62
     * Constructs an empty BoxAPIResponse without an associated HttpURLConnection.
63
     */
64
    public BoxAPIResponse() {
1✔
65
        this.contentLength = 0;
1✔
66
        this.contentType = null;
1✔
67
        this.requestMethod = null;
1✔
68
        this.requestUrl = null;
1✔
69
    }
1✔
70

71
    /**
72
     * Constructs a BoxAPIResponse with a http response code and response headers.
73
     *
74
     * @param responseCode http response code
75
     * @param headers      map of headers
76
     */
77
    public BoxAPIResponse(
78
        int responseCode, String requestMethod, String requestUrl, Map<String, List<String>> headers
79
    ) {
80
        this(responseCode, requestMethod, requestUrl, headers, null, null, 0);
1✔
81
    }
1✔
82

83
    public BoxAPIResponse(int code,
84
                          String requestMethod,
85
                          String requestUrl,
86
                          Map<String, List<String>> headers,
87
                          InputStream body,
88
                          String contentType,
89
                          long contentLength
90
    ) {
1✔
91
        this.responseCode = code;
1✔
92
        this.requestMethod = requestMethod;
1✔
93
        this.requestUrl = requestUrl;
1✔
94
        if (headers != null) {
1✔
95
            this.headers.putAll(headers);
1✔
96
        }
97
        this.rawInputStream = body;
1✔
98
        this.contentType = contentType;
1✔
99
        this.contentLength = contentLength;
1✔
100
        storeBodyResponse(body);
1✔
101
        if (isSuccess(responseCode)) {
1✔
102
            this.logResponse();
1✔
103
        } else {
104
            this.logErrorResponse(this.responseCode);
×
105
            throw new BoxAPIResponseException("The API returned an error code", responseCode, null, headers);
×
106
        }
107
    }
1✔
108

109
    private void storeBodyResponse(InputStream body) {
110
        try {
111
            if (contentType != null && body != null && contentType.contains(APPLICATION_JSON) && body.available() > 0) {
1✔
112
                InputStreamReader reader = new InputStreamReader(this.getBody(), UTF_8);
1✔
113
                StringBuilder builder = new StringBuilder();
1✔
114
                char[] buffer = new char[BUFFER_SIZE];
1✔
115

116
                int read = reader.read(buffer, 0, BUFFER_SIZE);
1✔
117
                while (read != -1) {
1✔
118
                    builder.append(buffer, 0, read);
1✔
119
                    read = reader.read(buffer, 0, BUFFER_SIZE);
1✔
120
                }
121
                reader.close();
1✔
122
                this.disconnect();
1✔
123
                bodyString = builder.toString();
1✔
124
                rawInputStream = new ByteArrayInputStream(bodyString.getBytes(UTF_8));
1✔
125
            }
126
        } catch (IOException e) {
×
127
            throw new RuntimeException("Cannot read body stream", e);
×
128
        }
1✔
129
    }
1✔
130

131
    private static boolean isSuccess(int responseCode) {
132
        return responseCode >= 200 && responseCode < 400;
1✔
133
    }
134

135
    static BoxAPIResponse toBoxResponse(Response response) {
136
        if (!response.isSuccessful() && !response.isRedirect()) {
1✔
137
            throw new BoxAPIResponseException(
1✔
138
                "The API returned an error code",
139
                response.code(),
1✔
140
                Optional.ofNullable(response.body()).map(body -> {
1✔
141
                    try {
142
                        return body.string();
1✔
143
                    } catch (IOException e) {
×
144
                        throw new RuntimeException(e);
×
145
                    }
146
                }).orElse("Body was null"),
1✔
147
                response.headers().toMultimap()
1✔
148
            );
149
        }
150
        ResponseBody responseBody = response.body();
1✔
151
        if (responseBody.contentLength() == 0 || responseBody.contentType() == null) {
1✔
152
            return new BoxAPIResponse(response.code(),
1✔
153
                response.request().method(),
1✔
154
                response.request().url().toString(),
1✔
155
                response.headers().toMultimap()
1✔
156
            );
157
        }
158
        if (responseBody != null && responseBody.contentType() != null) {
1✔
159
            if (responseBody.contentType().toString().contains(APPLICATION_JSON)) {
1✔
160
                String bodyAsString = "";
1✔
161
                try {
162
                    bodyAsString = responseBody.string();
1✔
163
                    return new BoxJSONResponse(response.code(),
1✔
164
                        response.request().method(),
1✔
165
                        response.request().url().toString(),
1✔
166
                        response.headers().toMultimap(),
1✔
167
                        Json.parse(bodyAsString).asObject()
1✔
168
                    );
169
                } catch (ParseException e) {
1✔
170
                    throw new BoxAPIException(format("Error parsing JSON:\n%s", bodyAsString), e);
1✔
171
                } catch (IOException e) {
×
172
                    throw new RuntimeException("Error getting response to string", e);
×
173
                }
174
            }
175
        }
176
        return new BoxAPIResponse(response.code(),
1✔
177
            response.request().method(),
1✔
178
            response.request().url().toString(),
1✔
179
            response.headers().toMultimap(),
1✔
180
            responseBody.byteStream(),
1✔
181
            Optional.ofNullable(responseBody.contentType()).map(MediaType::toString).orElse(null),
1✔
182
            responseBody.contentLength()
1✔
183
        );
184
    }
185

186
    /**
187
     * Gets the response code returned by the API.
188
     *
189
     * @return the response code returned by the API.
190
     */
191
    public int getResponseCode() {
192
        return this.responseCode;
1✔
193
    }
194

195
    /**
196
     * Gets the length of this response's body as indicated by the "Content-Length" header.
197
     *
198
     * @return the length of the response's body.
199
     */
200
    public long getContentLength() {
201
        return this.contentLength;
×
202
    }
203

204
    /**
205
     * Gets the value of the given header field.
206
     *
207
     * @param fieldName name of the header field.
208
     * @return value of the header.
209
     */
210
    public String getHeaderField(String fieldName) {
211
        return Optional.ofNullable(this.headers.get(fieldName)).map((l) -> l.get(0)).orElse("");
1✔
212
    }
213

214
    /**
215
     * Gets an InputStream for reading this response's body.
216
     *
217
     * @return an InputStream for reading the response's body.
218
     */
219
    public InputStream getBody() {
220
        return this.getBody(null);
1✔
221
    }
222

223
    /**
224
     * Gets an InputStream for reading this response's body which will report its read progress to a ProgressListener.
225
     *
226
     * @param listener a listener for monitoring the read progress of the body.
227
     * @return an InputStream for reading the response's body.
228
     */
229
    public InputStream getBody(ProgressListener listener) {
230
        if (this.inputStream == null) {
1✔
231
            if (listener == null) {
1✔
232
                this.inputStream = this.rawInputStream;
1✔
233
            } else {
234
                this.inputStream = new ProgressInputStream(this.rawInputStream, listener, this.getContentLength());
×
235
            }
236
        }
237
        return this.inputStream;
1✔
238
    }
239

240
    /**
241
     * Disconnects this response from the server and frees up any network resources. The body of this response can no
242
     * longer be read after it has been disconnected.
243
     */
244
    public void disconnect() {
245
        this.close();
1✔
246
    }
1✔
247

248
    /**
249
     * @return A Map containg headers on this Box API Response.
250
     */
251
    public Map<String, List<String>> getHeaders() {
252
        return this.headers;
1✔
253
    }
254

255
    @Override
256
    public String toString() {
257
        String lineSeparator = System.getProperty("line.separator");
1✔
258
        StringBuilder builder = new StringBuilder();
1✔
259
        builder.append("Response")
1✔
260
            .append(lineSeparator)
1✔
261
            .append(this.requestMethod)
1✔
262
            .append(' ')
1✔
263
            .append(this.requestUrl)
1✔
264
            .append(lineSeparator)
1✔
265
            .append(contentType != null ? "Content-Type: " + contentType + lineSeparator : "")
1✔
266
            .append(headers.isEmpty() ? "" : "Headers:" + lineSeparator);
1✔
267
        headers.entrySet()
1✔
268
            .stream()
1✔
269
            .filter(Objects::nonNull)
1✔
270
            .forEach(e -> builder.append(format("%s: [%s]%s", e.getKey().toLowerCase(), e.getValue(), lineSeparator)));
1✔
271

272
        String bodyString = this.bodyToString();
1✔
273
        if (bodyString != null && !bodyString.equals("")) {
1✔
274
            builder.append("Body:").append(lineSeparator).append(bodyString);
1✔
275
        }
276

277
        return builder.toString().trim();
1✔
278
    }
279

280
    @Override
281
    public void close() {
282
        try {
283
            if (this.inputStream == null && this.rawInputStream != null) {
1✔
284
                this.rawInputStream.close();
×
285
            }
286
            if (this.inputStream != null) {
1✔
287
                this.inputStream.close();
1✔
288
            }
289
        } catch (IOException e) {
×
290
            throw new BoxAPIException(
×
291
                "Couldn't finish closing the connection to the Box API due to a network error or "
292
                    + "because the stream was already closed.", e
293
            );
294
        }
1✔
295
    }
1✔
296

297
    /**
298
     * Returns a string representation of this response's body. This method is used when logging this response's body.
299
     * By default, it returns an empty string (to avoid accidentally logging binary data) unless the response contained
300
     * an error message.
301
     *
302
     * @return a string representation of this response's body.
303
     */
304
    protected String bodyToString() {
305
        return this.bodyString;
1✔
306
    }
307

308
    private void logResponse() {
309
        if (LOGGER.isDebugEnabled()) {
1✔
310
            LOGGER.debug(this.toString());
×
311
        }
312
    }
1✔
313

314
    private void logErrorResponse(int responseCode) {
315
        if (responseCode < 500 && LOGGER.isWarnEnabled()) {
×
316
            LOGGER.warn(this.toString());
×
317
        }
318
        if (responseCode >= 500 && LOGGER.isErrorEnabled()) {
×
319
            LOGGER.error(this.toString());
×
320
        }
321
    }
×
322

323
    protected String getRequestMethod() {
324
        return requestMethod;
1✔
325
    }
326

327
    protected String getRequestUrl() {
328
        return requestUrl;
1✔
329
    }
330
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc