forked from googleapis/google-http-java-client
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHttpResponse.java
496 lines (453 loc) · 15.2 KB
/
HttpResponse.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/*
* Copyright (c) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.api.client.http;
import com.google.api.client.util.Charsets;
import com.google.api.client.util.IOUtils;
import com.google.api.client.util.LoggingInputStream;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
/**
* HTTP response.
*
* <p>Callers should call {@link #disconnect} when the HTTP response object is no longer needed.
* However, {@link #disconnect} does not have to be called if the response stream is properly
* closed. Example usage:
*
* <pre>
* HttpResponse response = request.execute();
* try {
* // process the HTTP response object
* } finally {
* response.disconnect();
* }
* </pre>
*
* <p>Implementation is not thread-safe.
*
* @since 1.0
* @author Yaniv Inbar
*/
public final class HttpResponse {
/** HTTP response content or {@code null} before {@link #getContent()}. */
private InputStream content;
/** Content encoding or {@code null}. */
private final String contentEncoding;
/** Content type or {@code null} for none. */
private final String contentType;
/** Parsed content-type/media type or {@code null} if content-type is null. */
private final HttpMediaType mediaType;
/** Low-level HTTP response. */
LowLevelHttpResponse response;
/** Status code. */
private final int statusCode;
/** Status message or {@code null}. */
private final String statusMessage;
/** HTTP request. */
private final HttpRequest request;
/** Whether {@link #getContent()} should return raw input stream. */
private final boolean returnRawInputStream;
/** Content encoding for GZip */
private static final String CONTENT_ENCODING_GZIP = "gzip";
/** Content encoding for GZip (legacy) */
private static final String CONTENT_ENCODING_XGZIP = "x-gzip";
/**
* Determines the limit to the content size that will be logged during {@link #getContent()}.
*
* <p>Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
* <p>If the content size is greater than this limit then it will not be logged.
*
* <p>Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
* <p>Defaults to {@link HttpRequest#getContentLoggingLimit()}.
*/
private int contentLoggingLimit;
/**
* Determines whether logging should be enabled on this response.
*
* <p>Defaults to {@link HttpRequest#isLoggingEnabled()}.
*/
private boolean loggingEnabled;
/** Signals whether the content has been read from the input stream. */
private boolean contentRead;
HttpResponse(HttpRequest request, LowLevelHttpResponse response) throws IOException {
this.request = request;
this.returnRawInputStream = request.getResponseReturnRawInputStream();
contentLoggingLimit = request.getContentLoggingLimit();
loggingEnabled = request.isLoggingEnabled();
this.response = response;
contentEncoding = response.getContentEncoding();
int code = response.getStatusCode();
statusCode = code < 0 ? 0 : code;
String message = response.getReasonPhrase();
statusMessage = message;
Logger logger = HttpTransport.LOGGER;
boolean loggable = loggingEnabled && logger.isLoggable(Level.CONFIG);
StringBuilder logbuf = null;
if (loggable) {
logbuf = new StringBuilder();
logbuf.append("-------------- RESPONSE --------------").append(StringUtils.LINE_SEPARATOR);
String statusLine = response.getStatusLine();
if (statusLine != null) {
logbuf.append(statusLine);
} else {
logbuf.append(statusCode);
if (message != null) {
logbuf.append(' ').append(message);
}
}
logbuf.append(StringUtils.LINE_SEPARATOR);
}
// headers
request.getResponseHeaders().fromHttpResponse(response, loggable ? logbuf : null);
// Retrieve the content-type directly from the headers as response.getContentType() is outdated
// and e.g. not set by BatchUnparsedResponse.FakeLowLevelHttpResponse
String contentType = response.getContentType();
if (contentType == null) {
contentType = request.getResponseHeaders().getContentType();
}
this.contentType = contentType;
mediaType = contentType == null ? null : new HttpMediaType(contentType);
// log from buffer
if (loggable) {
logger.config(logbuf.toString());
}
}
/**
* Returns the limit to the content size that will be logged during {@link #getContent()}.
*
* <p>Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
* <p>If the content size is greater than this limit then it will not be logged.
*
* <p>Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
* <p>Defaults to {@link HttpRequest#getContentLoggingLimit()}.
*
* @since 1.7
*/
public int getContentLoggingLimit() {
return contentLoggingLimit;
}
/**
* Set the limit to the content size that will be logged during {@link #getContent()}.
*
* <p>Content will only be logged if {@link #isLoggingEnabled} is {@code true}.
*
* <p>If the content size is greater than this limit then it will not be logged.
*
* <p>Can be set to {@code 0} to disable content logging. This is useful for example if content
* has sensitive data such as authentication information.
*
* <p>Defaults to {@link HttpRequest#getContentLoggingLimit()}.
*
* @since 1.7
*/
public HttpResponse setContentLoggingLimit(int contentLoggingLimit) {
Preconditions.checkArgument(
contentLoggingLimit >= 0, "The content logging limit must be non-negative.");
this.contentLoggingLimit = contentLoggingLimit;
return this;
}
/**
* Returns whether logging should be enabled on this response.
*
* <p>Defaults to {@link HttpRequest#isLoggingEnabled()}.
*
* @since 1.9
*/
public boolean isLoggingEnabled() {
return loggingEnabled;
}
/**
* Sets whether logging should be enabled on this response.
*
* <p>Defaults to {@link HttpRequest#isLoggingEnabled()}.
*
* @since 1.9
*/
public HttpResponse setLoggingEnabled(boolean loggingEnabled) {
this.loggingEnabled = loggingEnabled;
return this;
}
/**
* Returns the content encoding or {@code null} for none.
*
* @since 1.5
*/
public String getContentEncoding() {
return contentEncoding;
}
/**
* Returns the content type or {@code null} for none.
*
* @since 1.5
*/
public String getContentType() {
return contentType;
}
/**
* Returns the parsed Content-Type in form of a {@link HttpMediaType} or {@code null} if no
* content-type was set.
*
* @since 1.10
*/
public HttpMediaType getMediaType() {
return mediaType;
}
/**
* Returns the HTTP response headers.
*
* @since 1.5
*/
public HttpHeaders getHeaders() {
return request.getResponseHeaders();
}
/**
* Returns whether received a successful HTTP status code {@code >= 200 && < 300} (see {@link
* #getStatusCode()}).
*
* @since 1.5
*/
public boolean isSuccessStatusCode() {
return HttpStatusCodes.isSuccess(statusCode);
}
/**
* Returns the HTTP status code or {@code 0} for none.
*
* @since 1.5
*/
public int getStatusCode() {
return statusCode;
}
/**
* Returns the HTTP status message or {@code null} for none.
*
* @since 1.5
*/
public String getStatusMessage() {
return statusMessage;
}
/**
* Returns the HTTP transport.
*
* @since 1.5
*/
public HttpTransport getTransport() {
return request.getTransport();
}
/**
* Returns the HTTP request.
*
* @since 1.5
*/
public HttpRequest getRequest() {
return request;
}
/**
* Returns the content of the HTTP response.
*
* <p>The result is cached, so subsequent calls will be fast.
*
* <p>Callers should call {@link InputStream#close} after the returned {@link InputStream} is no
* longer needed. Example usage:
*
* <pre>
* InputStream is = response.getContent();
* try {
* // Process the input stream..
* } finally {
* is.close();
* }
* </pre>
*
* <p>{@link HttpResponse#disconnect} does not have to be called if the content is closed.
*
* @return input stream content of the HTTP response or {@code null} for none
* @throws IOException I/O exception
*/
public InputStream getContent() throws IOException {
if (!contentRead) {
InputStream lowLevelResponseContent = this.response.getContent();
if (lowLevelResponseContent != null) {
// Flag used to indicate if an exception is thrown before the content is successfully
// processed.
boolean contentProcessed = false;
try {
// gzip encoding (wrap content with GZipInputStream)
if (!returnRawInputStream && this.contentEncoding != null) {
final String oontentEncoding = this.contentEncoding.trim().toLowerCase(Locale.ENGLISH);
if (CONTENT_ENCODING_GZIP.equals(oontentEncoding) || CONTENT_ENCODING_XGZIP.equals(oontentEncoding)) {
lowLevelResponseContent =
new ConsumingInputStream(new GZIPInputStream(lowLevelResponseContent));
}
}
// logging (wrap content with LoggingInputStream)
Logger logger = HttpTransport.LOGGER;
if (loggingEnabled && logger.isLoggable(Level.CONFIG)) {
lowLevelResponseContent =
new LoggingInputStream(
lowLevelResponseContent, logger, Level.CONFIG, contentLoggingLimit);
}
content = lowLevelResponseContent;
contentProcessed = true;
} catch (EOFException e) {
// this may happen for example on a HEAD request since there no actual response data read
// in GZIPInputStream
} finally {
if (!contentProcessed) {
lowLevelResponseContent.close();
}
}
}
contentRead = true;
}
return content;
}
/**
* Writes the content of the HTTP response into the given destination output stream.
*
* <p>Sample usage:
*
* <pre>
* HttpRequest request = requestFactory.buildGetRequest(
* new GenericUrl("https://www.google.com/images/srpr/logo3w.png"));
* OutputStream outputStream = new FileOutputStream(new File("/tmp/logo3w.png"));
* try {
* HttpResponse response = request.execute();
* response.download(outputStream);
* } finally {
* outputStream.close();
* }
* </pre>
*
* <p>This method closes the content of the HTTP response from {@link #getContent()}.
*
* <p>This method does not close the given output stream.
*
* @param outputStream destination output stream
* @throws IOException I/O exception
* @since 1.9
*/
public void download(OutputStream outputStream) throws IOException {
InputStream inputStream = getContent();
IOUtils.copy(inputStream, outputStream);
}
/** Closes the content of the HTTP response from {@link #getContent()}, ignoring any content. */
public void ignore() throws IOException {
InputStream content = getContent();
if (content != null) {
content.close();
}
}
/**
* Close the HTTP response content using {@link #ignore}, and disconnect using {@link
* LowLevelHttpResponse#disconnect()}.
*
* @since 1.4
*/
public void disconnect() throws IOException {
ignore();
response.disconnect();
}
/**
* Parses the content of the HTTP response from {@link #getContent()} and reads it into a data
* class of key/value pairs using the parser returned by {@link HttpRequest#getParser()}.
*
* <p><b>Reference:</b> http://tools.ietf.org/html/rfc2616#section-4.3
*
* @return parsed data class or {@code null} for no content
*/
public <T> T parseAs(Class<T> dataClass) throws IOException {
if (!hasMessageBody()) {
return null;
}
return request.getParser().parseAndClose(getContent(), getContentCharset(), dataClass);
}
/**
* Returns whether this response contains a message body as specified in {@href
* http://tools.ietf.org/html/rfc2616#section-4.3}, calling {@link #ignore()} if {@code false}.
*/
private boolean hasMessageBody() throws IOException {
int statusCode = getStatusCode();
if (getRequest().getRequestMethod().equals(HttpMethods.HEAD)
|| statusCode / 100 == 1
|| statusCode == HttpStatusCodes.STATUS_CODE_NO_CONTENT
|| statusCode == HttpStatusCodes.STATUS_CODE_NOT_MODIFIED) {
ignore();
return false;
}
return true;
}
/**
* Parses the content of the HTTP response from {@link #getContent()} and reads it into a data
* type of key/value pairs using the parser returned by {@link HttpRequest#getParser()}.
*
* @return parsed data type instance or {@code null} for no content
* @since 1.10
*/
public Object parseAs(Type dataType) throws IOException {
if (!hasMessageBody()) {
return null;
}
return request.getParser().parseAndClose(getContent(), getContentCharset(), dataType);
}
/**
* Parses the content of the HTTP response from {@link #getContent()} and reads it into a string.
*
* <p>Since this method returns {@code ""} for no content, a simpler check for no content is to
* check if {@link #getContent()} is {@code null}.
*
* <p>All content is read from the input content stream rather than being limited by the
* Content-Length. For the character set, it follows the specification by parsing the "charset"
* parameter of the Content-Type header or by default {@code "ISO-8859-1"} if the parameter is
* missing.
*
* @return parsed string or {@code ""} for no content
* @throws IOException I/O exception
*/
public String parseAsString() throws IOException {
InputStream content = getContent();
if (content == null) {
return "";
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(content, out);
return out.toString(getContentCharset().name());
}
/**
* Returns the {@link Charset} specified in the Content-Type of this response or the {@code
* "ISO-8859-1"} charset as a default.
*
* @since 1.10
*/
public Charset getContentCharset() {
return mediaType == null || mediaType.getCharsetParameter() == null
? Charsets.ISO_8859_1
: mediaType.getCharsetParameter();
}
}