|
1 /* |
|
2 * ==================================================================== |
|
3 * Licensed to the Apache Software Foundation (ASF) under one |
|
4 * or more contributor license agreements. See the NOTICE file |
|
5 * distributed with this work for additional information |
|
6 * regarding copyright ownership. The ASF licenses this file |
|
7 * to you under the Apache License, Version 2.0 (the |
|
8 * "License"); you may not use this file except in compliance |
|
9 * with the License. You may obtain a copy of the License at |
|
10 * |
|
11 * http://www.apache.org/licenses/LICENSE-2.0 |
|
12 * |
|
13 * Unless required by applicable law or agreed to in writing, |
|
14 * software distributed under the License is distributed on an |
|
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
|
16 * KIND, either express or implied. See the License for the |
|
17 * specific language governing permissions and limitations |
|
18 * under the License. |
|
19 * ==================================================================== |
|
20 * |
|
21 * This software consists of voluntary contributions made by many |
|
22 * individuals on behalf of the Apache Software Foundation. For more |
|
23 * information on the Apache Software Foundation, please see |
|
24 * <http://www.apache.org/>. |
|
25 * |
|
26 */ |
|
27 |
|
28 package ch.boye.httpclientandroidlib.protocol; |
|
29 |
|
30 import java.io.IOException; |
|
31 import java.net.ProtocolException; |
|
32 |
|
33 import ch.boye.httpclientandroidlib.HttpClientConnection; |
|
34 import ch.boye.httpclientandroidlib.HttpEntity; |
|
35 import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest; |
|
36 import ch.boye.httpclientandroidlib.HttpException; |
|
37 import ch.boye.httpclientandroidlib.HttpRequest; |
|
38 import ch.boye.httpclientandroidlib.HttpResponse; |
|
39 import ch.boye.httpclientandroidlib.HttpStatus; |
|
40 import ch.boye.httpclientandroidlib.HttpVersion; |
|
41 import ch.boye.httpclientandroidlib.ProtocolVersion; |
|
42 import ch.boye.httpclientandroidlib.params.CoreProtocolPNames; |
|
43 |
|
44 /** |
|
45 * HttpRequestExecutor is a client side HTTP protocol handler based on the |
|
46 * blocking I/O model that implements the essential requirements of the HTTP |
|
47 * protocol for the client side message processing, as described by RFC 2616. |
|
48 * <br> |
|
49 * HttpRequestExecutor relies on {@link HttpProcessor} to generate mandatory |
|
50 * protocol headers for all outgoing messages and apply common, cross-cutting |
|
51 * message transformations to all incoming and outgoing messages. Application |
|
52 * specific processing can be implemented outside HttpRequestExecutor once the |
|
53 * request has been executed and a response has been received. |
|
54 * <p> |
|
55 * The following parameters can be used to customize the behavior of this |
|
56 * class: |
|
57 * <ul> |
|
58 * <li>{@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#WAIT_FOR_CONTINUE}</li> |
|
59 * </ul> |
|
60 * |
|
61 * @since 4.0 |
|
62 */ |
|
63 public class HttpRequestExecutor { |
|
64 |
|
65 /** |
|
66 * Create a new request executor. |
|
67 */ |
|
68 public HttpRequestExecutor() { |
|
69 super(); |
|
70 } |
|
71 |
|
72 /** |
|
73 * Decide whether a response comes with an entity. |
|
74 * The implementation in this class is based on RFC 2616. |
|
75 * <br/> |
|
76 * Derived executors can override this method to handle |
|
77 * methods and response codes not specified in RFC 2616. |
|
78 * |
|
79 * @param request the request, to obtain the executed method |
|
80 * @param response the response, to obtain the status code |
|
81 */ |
|
82 protected boolean canResponseHaveBody(final HttpRequest request, |
|
83 final HttpResponse response) { |
|
84 |
|
85 if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { |
|
86 return false; |
|
87 } |
|
88 int status = response.getStatusLine().getStatusCode(); |
|
89 return status >= HttpStatus.SC_OK |
|
90 && status != HttpStatus.SC_NO_CONTENT |
|
91 && status != HttpStatus.SC_NOT_MODIFIED |
|
92 && status != HttpStatus.SC_RESET_CONTENT; |
|
93 } |
|
94 |
|
95 /** |
|
96 * Sends the request and obtain a response. |
|
97 * |
|
98 * @param request the request to execute. |
|
99 * @param conn the connection over which to execute the request. |
|
100 * |
|
101 * @return the response to the request. |
|
102 * |
|
103 * @throws IOException in case of an I/O error. |
|
104 * @throws HttpException in case of HTTP protocol violation or a processing |
|
105 * problem. |
|
106 */ |
|
107 public HttpResponse execute( |
|
108 final HttpRequest request, |
|
109 final HttpClientConnection conn, |
|
110 final HttpContext context) |
|
111 throws IOException, HttpException { |
|
112 if (request == null) { |
|
113 throw new IllegalArgumentException("HTTP request may not be null"); |
|
114 } |
|
115 if (conn == null) { |
|
116 throw new IllegalArgumentException("Client connection may not be null"); |
|
117 } |
|
118 if (context == null) { |
|
119 throw new IllegalArgumentException("HTTP context may not be null"); |
|
120 } |
|
121 |
|
122 try { |
|
123 HttpResponse response = doSendRequest(request, conn, context); |
|
124 if (response == null) { |
|
125 response = doReceiveResponse(request, conn, context); |
|
126 } |
|
127 return response; |
|
128 } catch (IOException ex) { |
|
129 closeConnection(conn); |
|
130 throw ex; |
|
131 } catch (HttpException ex) { |
|
132 closeConnection(conn); |
|
133 throw ex; |
|
134 } catch (RuntimeException ex) { |
|
135 closeConnection(conn); |
|
136 throw ex; |
|
137 } |
|
138 } |
|
139 |
|
140 private final static void closeConnection(final HttpClientConnection conn) { |
|
141 try { |
|
142 conn.close(); |
|
143 } catch (IOException ignore) { |
|
144 } |
|
145 } |
|
146 |
|
147 /** |
|
148 * Pre-process the given request using the given protocol processor and |
|
149 * initiates the process of request execution. |
|
150 * |
|
151 * @param request the request to prepare |
|
152 * @param processor the processor to use |
|
153 * @param context the context for sending the request |
|
154 * |
|
155 * @throws IOException in case of an I/O error. |
|
156 * @throws HttpException in case of HTTP protocol violation or a processing |
|
157 * problem. |
|
158 */ |
|
159 public void preProcess( |
|
160 final HttpRequest request, |
|
161 final HttpProcessor processor, |
|
162 final HttpContext context) |
|
163 throws HttpException, IOException { |
|
164 if (request == null) { |
|
165 throw new IllegalArgumentException("HTTP request may not be null"); |
|
166 } |
|
167 if (processor == null) { |
|
168 throw new IllegalArgumentException("HTTP processor may not be null"); |
|
169 } |
|
170 if (context == null) { |
|
171 throw new IllegalArgumentException("HTTP context may not be null"); |
|
172 } |
|
173 context.setAttribute(ExecutionContext.HTTP_REQUEST, request); |
|
174 processor.process(request, context); |
|
175 } |
|
176 |
|
177 /** |
|
178 * Send the given request over the given connection. |
|
179 * <p> |
|
180 * This method also handles the expect-continue handshake if necessary. |
|
181 * If it does not have to handle an expect-continue handshake, it will |
|
182 * not use the connection for reading or anything else that depends on |
|
183 * data coming in over the connection. |
|
184 * |
|
185 * @param request the request to send, already |
|
186 * {@link #preProcess preprocessed} |
|
187 * @param conn the connection over which to send the request, |
|
188 * already established |
|
189 * @param context the context for sending the request |
|
190 * |
|
191 * @return a terminal response received as part of an expect-continue |
|
192 * handshake, or |
|
193 * <code>null</code> if the expect-continue handshake is not used |
|
194 * |
|
195 * @throws IOException in case of an I/O error. |
|
196 * @throws HttpException in case of HTTP protocol violation or a processing |
|
197 * problem. |
|
198 */ |
|
199 protected HttpResponse doSendRequest( |
|
200 final HttpRequest request, |
|
201 final HttpClientConnection conn, |
|
202 final HttpContext context) |
|
203 throws IOException, HttpException { |
|
204 if (request == null) { |
|
205 throw new IllegalArgumentException("HTTP request may not be null"); |
|
206 } |
|
207 if (conn == null) { |
|
208 throw new IllegalArgumentException("HTTP connection may not be null"); |
|
209 } |
|
210 if (context == null) { |
|
211 throw new IllegalArgumentException("HTTP context may not be null"); |
|
212 } |
|
213 |
|
214 HttpResponse response = null; |
|
215 |
|
216 context.setAttribute(ExecutionContext.HTTP_CONNECTION, conn); |
|
217 context.setAttribute(ExecutionContext.HTTP_REQ_SENT, Boolean.FALSE); |
|
218 |
|
219 conn.sendRequestHeader(request); |
|
220 if (request instanceof HttpEntityEnclosingRequest) { |
|
221 // Check for expect-continue handshake. We have to flush the |
|
222 // headers and wait for an 100-continue response to handle it. |
|
223 // If we get a different response, we must not send the entity. |
|
224 boolean sendentity = true; |
|
225 final ProtocolVersion ver = |
|
226 request.getRequestLine().getProtocolVersion(); |
|
227 if (((HttpEntityEnclosingRequest) request).expectContinue() && |
|
228 !ver.lessEquals(HttpVersion.HTTP_1_0)) { |
|
229 |
|
230 conn.flush(); |
|
231 // As suggested by RFC 2616 section 8.2.3, we don't wait for a |
|
232 // 100-continue response forever. On timeout, send the entity. |
|
233 int tms = request.getParams().getIntParameter( |
|
234 CoreProtocolPNames.WAIT_FOR_CONTINUE, 2000); |
|
235 |
|
236 if (conn.isResponseAvailable(tms)) { |
|
237 response = conn.receiveResponseHeader(); |
|
238 if (canResponseHaveBody(request, response)) { |
|
239 conn.receiveResponseEntity(response); |
|
240 } |
|
241 int status = response.getStatusLine().getStatusCode(); |
|
242 if (status < 200) { |
|
243 if (status != HttpStatus.SC_CONTINUE) { |
|
244 throw new ProtocolException( |
|
245 "Unexpected response: " + response.getStatusLine()); |
|
246 } |
|
247 // discard 100-continue |
|
248 response = null; |
|
249 } else { |
|
250 sendentity = false; |
|
251 } |
|
252 } |
|
253 } |
|
254 if (sendentity) { |
|
255 conn.sendRequestEntity((HttpEntityEnclosingRequest) request); |
|
256 } |
|
257 } |
|
258 conn.flush(); |
|
259 context.setAttribute(ExecutionContext.HTTP_REQ_SENT, Boolean.TRUE); |
|
260 return response; |
|
261 } |
|
262 |
|
263 /** |
|
264 * Waits for and receives a response. |
|
265 * This method will automatically ignore intermediate responses |
|
266 * with status code 1xx. |
|
267 * |
|
268 * @param request the request for which to obtain the response |
|
269 * @param conn the connection over which the request was sent |
|
270 * @param context the context for receiving the response |
|
271 * |
|
272 * @return the terminal response, not yet post-processed |
|
273 * |
|
274 * @throws IOException in case of an I/O error. |
|
275 * @throws HttpException in case of HTTP protocol violation or a processing |
|
276 * problem. |
|
277 */ |
|
278 protected HttpResponse doReceiveResponse( |
|
279 final HttpRequest request, |
|
280 final HttpClientConnection conn, |
|
281 final HttpContext context) |
|
282 throws HttpException, IOException { |
|
283 if (request == null) { |
|
284 throw new IllegalArgumentException("HTTP request may not be null"); |
|
285 } |
|
286 if (conn == null) { |
|
287 throw new IllegalArgumentException("HTTP connection may not be null"); |
|
288 } |
|
289 if (context == null) { |
|
290 throw new IllegalArgumentException("HTTP context may not be null"); |
|
291 } |
|
292 |
|
293 HttpResponse response = null; |
|
294 int statuscode = 0; |
|
295 |
|
296 while (response == null || statuscode < HttpStatus.SC_OK) { |
|
297 |
|
298 response = conn.receiveResponseHeader(); |
|
299 if (canResponseHaveBody(request, response)) { |
|
300 conn.receiveResponseEntity(response); |
|
301 } |
|
302 statuscode = response.getStatusLine().getStatusCode(); |
|
303 |
|
304 } // while intermediate response |
|
305 |
|
306 return response; |
|
307 |
|
308 } |
|
309 |
|
310 /** |
|
311 * Post-processes the given response using the given protocol processor and |
|
312 * completes the process of request execution. |
|
313 * <p> |
|
314 * This method does <i>not</i> read the response entity, if any. |
|
315 * The connection over which content of the response entity is being |
|
316 * streamed from cannot be reused until {@link HttpEntity#consumeContent()} |
|
317 * has been invoked. |
|
318 * |
|
319 * @param response the response object to post-process |
|
320 * @param processor the processor to use |
|
321 * @param context the context for post-processing the response |
|
322 * |
|
323 * @throws IOException in case of an I/O error. |
|
324 * @throws HttpException in case of HTTP protocol violation or a processing |
|
325 * problem. |
|
326 */ |
|
327 public void postProcess( |
|
328 final HttpResponse response, |
|
329 final HttpProcessor processor, |
|
330 final HttpContext context) |
|
331 throws HttpException, IOException { |
|
332 if (response == null) { |
|
333 throw new IllegalArgumentException("HTTP response may not be null"); |
|
334 } |
|
335 if (processor == null) { |
|
336 throw new IllegalArgumentException("HTTP processor may not be null"); |
|
337 } |
|
338 if (context == null) { |
|
339 throw new IllegalArgumentException("HTTP context may not be null"); |
|
340 } |
|
341 context.setAttribute(ExecutionContext.HTTP_RESPONSE, response); |
|
342 processor.process(response, context); |
|
343 } |
|
344 |
|
345 } // class HttpRequestExecutor |