7
7
import java .io .Closeable ;
8
8
import java .io .IOException ;
9
9
import java .io .InputStream ;
10
- import java .util .*;
10
+ import java .util .ArrayList ;
11
+ import java .util .Collections ;
12
+ import java .util .Comparator ;
13
+ import java .util .List ;
14
+ import java .util .Map ;
15
+ import java .util .TreeMap ;
11
16
import java .util .zip .GZIPInputStream ;
12
17
13
18
import javax .annotation .CheckForNull ;
14
19
import javax .annotation .Nonnull ;
15
20
21
+ import static java .net .HttpURLConnection .HTTP_OK ;
22
+
16
23
/**
17
24
* Response information supplied when a response is received and before the body is processed.
18
25
* <p>
26
+ * During a request to GitHub, {@link GitHubConnector#send(GitHubConnectorRequest)} returns a
27
+ * {@link GitHubConnectorResponse}. This is processed to create a GitHubResponse.
28
+ * <p>
19
29
* Instances of this class are closed once the response is done being processed. This means that {@link #bodyStream()}
20
30
* will not be readable after a call is completed.
21
31
*
@@ -35,6 +45,11 @@ public abstract class GitHubConnectorResponse implements Closeable {
35
45
private final GitHubConnectorRequest request ;
36
46
@ Nonnull
37
47
private final Map <String , List <String >> headers ;
48
+ private boolean bodyStreamCalled = false ;
49
+ private InputStream bodyStream = null ;
50
+ private byte [] bodyBytes = null ;
51
+ private boolean isClosed = false ;
52
+ private boolean isBodyStreamRereadable ;
38
53
39
54
/**
40
55
* GitHubConnectorResponse constructor
@@ -58,6 +73,7 @@ protected GitHubConnectorResponse(@Nonnull GitHubConnectorRequest request,
58
73
caseInsensitiveMap .put (entry .getKey (), Collections .unmodifiableList (new ArrayList <>(entry .getValue ())));
59
74
}
60
75
this .headers = Collections .unmodifiableMap (caseInsensitiveMap );
76
+ this .isBodyStreamRereadable = false ;
61
77
}
62
78
63
79
/**
@@ -79,17 +95,72 @@ public String header(String name) {
79
95
/**
80
96
* The response body as an {@link InputStream}.
81
97
*
98
+ * When {@link #isBodyStreamRereadable} is false, {@link #bodyStream()} can only be called once and the returned
99
+ * stream should be assumed to be read-once and not resetable. This is the default behavior for HTTP_OK responses
100
+ * and significantly reduces memory usage.
101
+ *
102
+ * When {@link #isBodyStreamRereadable} is true, {@link #bodyStream()} can be called be called multiple times. The
103
+ * full stream data is read into a byte array during the first call. Each call returns a new stream backed by the
104
+ * same byte array. This uses more memory, but is required to enable rereading the body stream during trace logging,
105
+ * debugging, and error responses.
106
+ *
82
107
* @return the response body
83
108
* @throws IOException
84
109
* if response stream is null or an I/O Exception occurs.
85
110
*/
86
111
@ Nonnull
87
- public abstract InputStream bodyStream () throws IOException ;
112
+ public InputStream bodyStream () throws IOException {
113
+ synchronized (this ) {
114
+ if (isClosed ) {
115
+ throw new IOException ("Response is closed" );
116
+ }
117
+
118
+ if (bodyStreamCalled ) {
119
+ if (!isBodyStreamRereadable ()) {
120
+ throw new IOException ("Response body not rereadable" );
121
+ }
122
+ } else {
123
+ bodyStream = wrapStream (rawBodyStream ());
124
+ bodyStreamCalled = true ;
125
+ }
126
+
127
+ if (bodyStream == null ) {
128
+ throw new IOException ("Response body missing, stream null" );
129
+ } else if (!isBodyStreamRereadable ()) {
130
+ return bodyStream ;
131
+ }
132
+
133
+ // Load rereadable byte array
134
+ if (bodyBytes == null ) {
135
+ bodyBytes = IOUtils .toByteArray (bodyStream );
136
+ // Close the raw body stream after successfully reading
137
+ IOUtils .closeQuietly (bodyStream );
138
+ }
139
+
140
+ return new ByteArrayInputStream (bodyBytes );
141
+ }
142
+ }
88
143
89
144
/**
90
- * Gets the {@link GitHubConnectorRequest} for this response.
145
+ * Get the raw implementation specific body stream for this response.
146
+ *
147
+ * This method will only be called once to completion. If an exception is thrown by this method, it may be called
148
+ * multiple times.
91
149
*
92
- * @return the {@link GitHubConnectorRequest} for this response.
150
+ * The stream returned from this method will be closed when the response is closed or sooner. Inheriting classes do
151
+ * not need to close it.
152
+ *
153
+ * @return the stream for the raw response
154
+ * @throws IOException
155
+ * if an I/O Exception occurs.
156
+ */
157
+ @ CheckForNull
158
+ protected abstract InputStream rawBodyStream () throws IOException ;
159
+
160
+ /**
161
+ * Gets the {@link GitHubConnector} for this response.
162
+ *
163
+ * @return the {@link GitHubConnector} for this response.
93
164
*/
94
165
@ Nonnull
95
166
public GitHubConnectorRequest request () {
@@ -116,6 +187,56 @@ public Map<String, List<String>> allHeaders() {
116
187
return headers ;
117
188
}
118
189
190
+ /**
191
+ * The body stream rereadable state.
192
+ *
193
+ * Body stream defaults to read once for HTTP_OK responses (to reduce memory usage). For non-HTTP_OK responses, body
194
+ * stream is switched to rereadable (in-memory byte array) for error processing.
195
+ *
196
+ * Calling {@link #setBodyStreamRereadable()} will force {@link #isBodyStreamRereadable} to be true for this
197
+ * response regardless of {@link #statusCode} value.
198
+ *
199
+ * @return true when body stream is rereadable.
200
+ */
201
+ public boolean isBodyStreamRereadable () {
202
+ synchronized (this ) {
203
+ return isBodyStreamRereadable || statusCode != HTTP_OK ;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Force body stream to rereadable regardless of status code.
209
+ *
210
+ * Calling {@link #setBodyStreamRereadable()} will force {@link #isBodyStreamRereadable} to be true for this
211
+ * response regardless of {@link #statusCode} value.
212
+ *
213
+ * This is required to support body value logging during low-level tracing but should be avoided in general since it
214
+ * consumes significantly more memory.
215
+ *
216
+ * Will throw runtime exception if a non-rereadable body stream has already been returned from
217
+ * {@link #bodyStream()}.
218
+ */
219
+ public void setBodyStreamRereadable () {
220
+ synchronized (this ) {
221
+ if (bodyStreamCalled && !isBodyStreamRereadable ()) {
222
+ throw new RuntimeException ("bodyStream() already called in read-once mode" );
223
+ }
224
+ isBodyStreamRereadable = true ;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * {@inheritDoc}
230
+ */
231
+ @ Override
232
+ public void close () throws IOException {
233
+ synchronized (this ) {
234
+ IOUtils .closeQuietly (bodyStream );
235
+ isClosed = true ;
236
+ this .bodyBytes = null ;
237
+ }
238
+ }
239
+
119
240
/**
120
241
* Handles wrapping the body stream if indicated by the "Content-Encoding" header.
121
242
*
@@ -155,13 +276,12 @@ public final int parseInt(String name) throws NumberFormatException {
155
276
156
277
/**
157
278
* A ByteArrayResponse class
279
+ *
280
+ * @deprecated Inherit directly from {@link GitHubConnectorResponse}.
158
281
*/
282
+ @ Deprecated
159
283
public abstract static class ByteArrayResponse extends GitHubConnectorResponse {
160
284
161
- private boolean inputStreamRead = false ;
162
- private byte [] inputBytes = null ;
163
- private boolean isClosed = false ;
164
-
165
285
/**
166
286
* Constructor for ByteArray Response
167
287
*
@@ -177,52 +297,5 @@ protected ByteArrayResponse(@Nonnull GitHubConnectorRequest request,
177
297
@ Nonnull Map <String , List <String >> headers ) {
178
298
super (request , statusCode , headers );
179
299
}
180
-
181
- /**
182
- * {@inheritDoc}
183
- */
184
- @ Override
185
- @ Nonnull
186
- public InputStream bodyStream () throws IOException {
187
- if (isClosed ) {
188
- throw new IOException ("Response is closed" );
189
- }
190
- synchronized (this ) {
191
- if (!inputStreamRead ) {
192
- InputStream rawStream = rawBodyStream ();
193
- try (InputStream stream = wrapStream (rawStream )) {
194
- if (stream != null ) {
195
- inputBytes = IOUtils .toByteArray (stream );
196
- }
197
- }
198
- inputStreamRead = true ;
199
- }
200
- }
201
-
202
- if (inputBytes == null ) {
203
- throw new IOException ("Response body missing, stream null" );
204
- }
205
-
206
- return new ByteArrayInputStream (inputBytes );
207
- }
208
-
209
- /**
210
- * Get the raw implementation specific body stream for this response.
211
- *
212
- * This method will only be called once to completion. If an exception is thrown, it may be called multiple
213
- * times.
214
- *
215
- * @return the stream for the raw response
216
- * @throws IOException
217
- * if an I/O Exception occurs.
218
- */
219
- @ CheckForNull
220
- protected abstract InputStream rawBodyStream () throws IOException ;
221
-
222
- @ Override
223
- public void close () throws IOException {
224
- isClosed = true ;
225
- this .inputBytes = null ;
226
- }
227
300
}
228
301
}
0 commit comments