View Javadoc
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 org.apache.hc.core5.http.impl.io;
29  
30  import java.io.IOException;
31  
32  import org.apache.hc.core5.annotation.Contract;
33  import org.apache.hc.core5.annotation.ThreadingBehavior;
34  import org.apache.hc.core5.http.ClassicHttpRequest;
35  import org.apache.hc.core5.http.ClassicHttpResponse;
36  import org.apache.hc.core5.http.ConnectionReuseStrategy;
37  import org.apache.hc.core5.http.Header;
38  import org.apache.hc.core5.http.HeaderElements;
39  import org.apache.hc.core5.http.HttpEntity;
40  import org.apache.hc.core5.http.HttpException;
41  import org.apache.hc.core5.http.HttpHeaders;
42  import org.apache.hc.core5.http.HttpStatus;
43  import org.apache.hc.core5.http.HttpVersion;
44  import org.apache.hc.core5.http.ProtocolException;
45  import org.apache.hc.core5.http.ProtocolVersion;
46  import org.apache.hc.core5.http.UnsupportedHttpVersionException;
47  import org.apache.hc.core5.http.config.Http1Config;
48  import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
49  import org.apache.hc.core5.http.impl.Http1StreamListener;
50  import org.apache.hc.core5.http.io.HttpClientConnection;
51  import org.apache.hc.core5.http.io.HttpResponseInformationCallback;
52  import org.apache.hc.core5.http.message.MessageSupport;
53  import org.apache.hc.core5.http.message.StatusLine;
54  import org.apache.hc.core5.http.protocol.HttpContext;
55  import org.apache.hc.core5.http.protocol.HttpCoreContext;
56  import org.apache.hc.core5.http.protocol.HttpProcessor;
57  import org.apache.hc.core5.io.Closer;
58  import org.apache.hc.core5.util.Args;
59  import org.apache.hc.core5.util.Timeout;
60  
61  /**
62   * {@code HttpRequestExecutor} is a client side HTTP protocol handler based
63   * on the blocking (classic) I/O model.
64   * <p>
65   * {@code HttpRequestExecutor} relies on {@link HttpProcessor} to generate
66   * mandatory protocol headers for all outgoing messages and apply common,
67   * cross-cutting message transformations to all incoming and outgoing messages.
68   * Application specific processing can be implemented outside
69   * {@code HttpRequestExecutor} once the request has been executed and
70   * a response has been received.
71   *
72   * @since 4.0
73   */
74  @Contract(threading = ThreadingBehavior.IMMUTABLE)
75  public class HttpRequestExecutor {
76  
77      public static final Timeout DEFAULT_WAIT_FOR_CONTINUE = Timeout.ofSeconds(3);
78  
79      private final Http1Config http1Config;
80      private final ConnectionReuseStrategy connReuseStrategy;
81      private final Http1StreamListener streamListener;
82  
83      /**
84       * Creates new instance of HttpRequestExecutor.
85       *
86       * @since 5.3
87       */
88      public HttpRequestExecutor(
89              final Http1Config http1Config,
90              final ConnectionReuseStrategy connReuseStrategy,
91              final Http1StreamListener streamListener) {
92          super();
93          this.http1Config = http1Config != null ? http1Config : Http1Config.DEFAULT;
94          this.connReuseStrategy = connReuseStrategy != null ? connReuseStrategy : DefaultConnectionReuseStrategy.INSTANCE;
95          this.streamListener = streamListener;
96      }
97  
98      /**
99       * @deprecated Use {@link #HttpRequestExecutor(Http1Config, ConnectionReuseStrategy, Http1StreamListener)}
100      */
101     @Deprecated
102     public HttpRequestExecutor(
103             final Timeout waitForContinue,
104             final ConnectionReuseStrategy connReuseStrategy,
105             final Http1StreamListener streamListener) {
106         this(Http1Config.custom()
107                 .setWaitForContinueTimeout(waitForContinue)
108                 .build(),
109                 connReuseStrategy,
110                 streamListener);
111     }
112 
113     public HttpRequestExecutor(final ConnectionReuseStrategy connReuseStrategy) {
114         this(Http1Config.DEFAULT, connReuseStrategy, null);
115     }
116 
117     public HttpRequestExecutor() {
118         this(Http1Config.DEFAULT, null, null);
119     }
120 
121     /**
122      * Sends the request and obtain a response.
123      *
124      * @param request   the request to execute.
125      * @param conn      the connection over which to execute the request.
126      * @param informationCallback   callback to execute upon receipt of information status (1xx).
127      *                              May be null.
128      * @param localContext the context
129      * @return  the response to the request.
130      *
131      * @throws IOException in case of an I/O error.
132      * @throws HttpException in case of HTTP protocol violation or a processing
133      *   problem.
134      */
135     public ClassicHttpResponse execute(
136             final ClassicHttpRequest request,
137             final HttpClientConnection conn,
138             final HttpResponseInformationCallback informationCallback,
139             final HttpContext localContext) throws IOException, HttpException {
140         Args.notNull(request, "HTTP request");
141         Args.notNull(conn, "Client connection");
142         Args.notNull(localContext, "HTTP context");
143         final HttpCoreContext context = HttpCoreContext.castOrCreate(localContext);
144         try {
145             context.setSSLSession(conn.getSSLSession());
146             context.setEndpointDetails(conn.getEndpointDetails());
147 
148             conn.sendRequestHeader(request);
149             if (streamListener != null) {
150                 streamListener.onRequestHead(conn, request);
151             }
152             boolean expectContinue = false;
153             final HttpEntity entity = request.getEntity();
154             if (entity != null) {
155                 final Header expect = request.getFirstHeader(HttpHeaders.EXPECT);
156                 expectContinue = expect != null && HeaderElements.CONTINUE.equalsIgnoreCase(expect.getValue());
157                 if (!expectContinue) {
158                     conn.sendRequestEntity(request);
159                 }
160             }
161             conn.flush();
162             ClassicHttpResponse response = null;
163             while (response == null) {
164                 if (expectContinue) {
165                     final Timeout timeout = http1Config.getWaitForContinueTimeout() != null ? http1Config.getWaitForContinueTimeout() : DEFAULT_WAIT_FOR_CONTINUE;
166                     if (conn.isDataAvailable(timeout)) {
167                         response = conn.receiveResponseHeader();
168                         if (streamListener != null) {
169                             streamListener.onResponseHead(conn, response);
170                         }
171                         final int status = response.getCode();
172                         if (status == HttpStatus.SC_CONTINUE) {
173                             // discard 100-continue
174                             response = null;
175                             conn.sendRequestEntity(request);
176                         } else if (status < HttpStatus.SC_SUCCESS) {
177                             if (informationCallback != null) {
178                                 informationCallback.execute(response, conn, context);
179                             }
180                             response = null;
181                             continue;
182                         } else if (status >= HttpStatus.SC_CLIENT_ERROR){
183                             conn.terminateRequest(request);
184                         } else {
185                             conn.sendRequestEntity(request);
186                         }
187                     } else {
188                         conn.sendRequestEntity(request);
189                     }
190                     conn.flush();
191                     expectContinue = false;
192                 } else {
193                     response = conn.receiveResponseHeader();
194                     if (streamListener != null) {
195                         streamListener.onResponseHead(conn, response);
196                     }
197                     final int status = response.getCode();
198                     if (status < HttpStatus.SC_INFORMATIONAL) {
199                         throw new ProtocolException("Invalid response: " + new StatusLine(response));
200                     }
201                     if (status < HttpStatus.SC_SUCCESS) {
202                         if (informationCallback != null && status != HttpStatus.SC_CONTINUE) {
203                             informationCallback.execute(response, conn, context);
204                         }
205                         response = null;
206                     }
207                 }
208             }
209             if (MessageSupport.canResponseHaveBody(request.getMethod(), response)) {
210                 conn.receiveResponseEntity(response);
211             }
212             return response;
213 
214         } catch (final HttpException | IOException | RuntimeException ex) {
215             Closer.closeQuietly(conn);
216             throw ex;
217         }
218     }
219 
220     /**
221      * Sends the request and obtain a response.
222      *
223      * @param request   the request to execute.
224      * @param conn      the connection over which to execute the request.
225      * @param context the context
226      * @return  the response to the request.
227      *
228      * @throws IOException in case of an I/O error.
229      * @throws HttpException in case of HTTP protocol violation or a processing
230      *   problem.
231      */
232     public ClassicHttpResponse execute(
233             final ClassicHttpRequest request,
234             final HttpClientConnection conn,
235             final HttpContext context) throws IOException, HttpException {
236         return execute(request, conn, null, context);
237     }
238 
239     /**
240      * Pre-process the given request using the given protocol processor and
241      * initiates the process of request execution.
242      *
243      * @param request   the request to prepare
244      * @param processor the processor to use
245      * @param localContext the context for sending the request
246      *
247      * @throws IOException in case of an I/O error.
248      * @throws HttpException in case of HTTP protocol violation or a processing
249      *   problem.
250      */
251     public void preProcess(
252             final ClassicHttpRequest request,
253             final HttpProcessor processor,
254             final HttpContext localContext) throws HttpException, IOException {
255         Args.notNull(request, "HTTP request");
256         Args.notNull(processor, "HTTP processor");
257         Args.notNull(localContext, "HTTP context");
258         final ProtocolVersion transportVersion = request.getVersion();
259         if (transportVersion != null && !transportVersion.lessEquals(http1Config.getVersion())) {
260             throw new UnsupportedHttpVersionException(transportVersion);
261         }
262         final HttpCoreContext context = HttpCoreContext.cast(localContext);
263         context.setProtocolVersion(transportVersion != null ? transportVersion : http1Config.getVersion());
264         context.setRequest(request);
265         processor.process(request, request.getEntity(), context);
266     }
267 
268     /**
269      * Post-processes the given response using the given protocol processor and
270      * completes the process of request execution.
271      * <p>
272      * This method does <i>not</i> read the response entity, if any.
273      * The connection over which content of the response entity is being
274      * streamed from cannot be reused until the response entity has been
275      * fully consumed.
276      *
277      * @param response  the response object to post-process
278      * @param processor the processor to use
279      * @param localContext the context for post-processing the response
280      *
281      * @throws IOException in case of an I/O error.
282      * @throws HttpException in case of HTTP protocol violation or a processing
283      *   problem.
284      */
285     public void postProcess(
286             final ClassicHttpResponse response,
287             final HttpProcessor processor,
288             final HttpContext localContext) throws HttpException, IOException {
289         Args.notNull(response, "HTTP response");
290         Args.notNull(processor, "HTTP processor");
291         Args.notNull(localContext, "HTTP context");
292         final HttpCoreContext context = HttpCoreContext.cast(localContext);
293         final ProtocolVersion transportVersion = response.getVersion();
294         if (transportVersion != null) {
295             if (transportVersion.greaterEquals(HttpVersion.HTTP_2)) {
296                 throw new UnsupportedHttpVersionException(transportVersion);
297             }
298             context.setProtocolVersion(transportVersion);
299         }
300         context.setResponse(response);
301         processor.process(response, response.getEntity(), context);
302     }
303 
304     /**
305      * Determines whether the connection can be kept alive and is safe to be re-used for subsequent message exchanges.
306      *
307      * @param request current request object.
308      * @param response  current response object.
309      * @param connection actual connection.
310      * @param context current context.
311      * @return {@code true} is the connection can be kept-alive and re-used.
312      * @throws IOException in case of an I/O error.
313      */
314     public boolean keepAlive(
315             final ClassicHttpRequest request,
316             final ClassicHttpResponse response,
317             final HttpClientConnection connection,
318             final HttpContext context) throws IOException {
319         Args.notNull(connection, "HTTP connection");
320         Args.notNull(request, "HTTP request");
321         Args.notNull(response, "HTTP response");
322         Args.notNull(context, "HTTP context");
323         final boolean keepAlive = connection.isConsistent() && connReuseStrategy.keepAlive(request, response, context);
324         if (streamListener != null) {
325             streamListener.onExchangeComplete(connection, keepAlive);
326         }
327         return keepAlive;
328     }
329 
330     /**
331      * Create a new {@link Builder}.
332      *
333      * @since 5.2
334      */
335     public static Builder builder() {
336         return new Builder();
337     }
338 
339     /**
340      * Builder for {@link HttpRequestExecutor}.
341      *
342      * @since 5.2
343      */
344     public static final class Builder {
345 
346         private Timeout waitForContinue;
347         private ConnectionReuseStrategy connReuseStrategy;
348         private Http1StreamListener streamListener;
349 
350         private Builder() {}
351 
352         public Builder withWaitForContinue(final Timeout waitForContinue) {
353             this.waitForContinue = waitForContinue;
354             return this;
355         }
356 
357         public Builder withConnectionReuseStrategy(final ConnectionReuseStrategy connReuseStrategy) {
358             this.connReuseStrategy = connReuseStrategy;
359             return this;
360         }
361 
362         public Builder withHttp1StreamListener(final Http1StreamListener streamListener) {
363             this.streamListener = streamListener;
364             return this;
365         }
366         /**
367          * Create a new HTTP Request Executor.
368          *
369          * @since 5.2
370          */
371         public HttpRequestExecutor build() {
372             return new HttpRequestExecutor(
373                     waitForContinue,
374                     connReuseStrategy,
375                     streamListener);
376         }
377     }
378 
379 }