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 context 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 context) throws IOException, HttpException {
140         Args.notNull(request, "HTTP request");
141         Args.notNull(conn, "Client connection");
142         Args.notNull(context, "HTTP context");
143         try {
144             context.setAttribute(HttpCoreContext.SSL_SESSION, conn.getSSLSession());
145             context.setAttribute(HttpCoreContext.CONNECTION_ENDPOINT, conn.getEndpointDetails());
146 
147             conn.sendRequestHeader(request);
148             if (streamListener != null) {
149                 streamListener.onRequestHead(conn, request);
150             }
151             boolean expectContinue = false;
152             final HttpEntity entity = request.getEntity();
153             if (entity != null) {
154                 final Header expect = request.getFirstHeader(HttpHeaders.EXPECT);
155                 expectContinue = expect != null && HeaderElements.CONTINUE.equalsIgnoreCase(expect.getValue());
156                 if (!expectContinue) {
157                     conn.sendRequestEntity(request);
158                 }
159             }
160             conn.flush();
161             ClassicHttpResponse response = null;
162             while (response == null) {
163                 if (expectContinue) {
164                     final Timeout timeout = http1Config.getWaitForContinueTimeout() != null ? http1Config.getWaitForContinueTimeout() : DEFAULT_WAIT_FOR_CONTINUE;
165                     if (conn.isDataAvailable(timeout)) {
166                         response = conn.receiveResponseHeader();
167                         if (streamListener != null) {
168                             streamListener.onResponseHead(conn, response);
169                         }
170                         final int status = response.getCode();
171                         if (status == HttpStatus.SC_CONTINUE) {
172                             // discard 100-continue
173                             response = null;
174                             conn.sendRequestEntity(request);
175                         } else if (status < HttpStatus.SC_SUCCESS) {
176                             if (informationCallback != null) {
177                                 informationCallback.execute(response, conn, context);
178                             }
179                             response = null;
180                             continue;
181                         } else if (status >= HttpStatus.SC_CLIENT_ERROR){
182                             conn.terminateRequest(request);
183                         } else {
184                             conn.sendRequestEntity(request);
185                         }
186                     } else {
187                         conn.sendRequestEntity(request);
188                     }
189                     conn.flush();
190                     expectContinue = false;
191                 } else {
192                     response = conn.receiveResponseHeader();
193                     if (streamListener != null) {
194                         streamListener.onResponseHead(conn, response);
195                     }
196                     final int status = response.getCode();
197                     if (status < HttpStatus.SC_INFORMATIONAL) {
198                         throw new ProtocolException("Invalid response: " + new StatusLine(response));
199                     }
200                     if (status < HttpStatus.SC_SUCCESS) {
201                         if (informationCallback != null && status != HttpStatus.SC_CONTINUE) {
202                             informationCallback.execute(response, conn, context);
203                         }
204                         response = null;
205                     }
206                 }
207             }
208             if (MessageSupport.canResponseHaveBody(request.getMethod(), response)) {
209                 conn.receiveResponseEntity(response);
210             }
211             return response;
212 
213         } catch (final HttpException | IOException | RuntimeException ex) {
214             Closer.closeQuietly(conn);
215             throw ex;
216         }
217     }
218 
219     /**
220      * Sends the request and obtain a response.
221      *
222      * @param request   the request to execute.
223      * @param conn      the connection over which to execute the request.
224      * @param context the context
225      * @return  the response to the request.
226      *
227      * @throws IOException in case of an I/O error.
228      * @throws HttpException in case of HTTP protocol violation or a processing
229      *   problem.
230      */
231     public ClassicHttpResponse execute(
232             final ClassicHttpRequest request,
233             final HttpClientConnection conn,
234             final HttpContext context) throws IOException, HttpException {
235         return execute(request, conn, null, context);
236     }
237 
238     /**
239      * Pre-process the given request using the given protocol processor and
240      * initiates the process of request execution.
241      *
242      * @param request   the request to prepare
243      * @param processor the processor to use
244      * @param context   the context for sending the request
245      *
246      * @throws IOException in case of an I/O error.
247      * @throws HttpException in case of HTTP protocol violation or a processing
248      *   problem.
249      */
250     public void preProcess(
251             final ClassicHttpRequest request,
252             final HttpProcessor processor,
253             final HttpContext context) throws HttpException, IOException {
254         Args.notNull(request, "HTTP request");
255         Args.notNull(processor, "HTTP processor");
256         Args.notNull(context, "HTTP context");
257         final ProtocolVersion transportVersion = request.getVersion();
258         if (transportVersion != null && !transportVersion.lessEquals(http1Config.getVersion())) {
259             throw new UnsupportedHttpVersionException(transportVersion);
260         }
261         context.setProtocolVersion(transportVersion != null ? transportVersion : http1Config.getVersion());
262         context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
263         processor.process(request, request.getEntity(), context);
264     }
265 
266     /**
267      * Post-processes the given response using the given protocol processor and
268      * completes the process of request execution.
269      * <p>
270      * This method does <i>not</i> read the response entity, if any.
271      * The connection over which content of the response entity is being
272      * streamed from cannot be reused until the response entity has been
273      * fully consumed.
274      *
275      * @param response  the response object to post-process
276      * @param processor the processor to use
277      * @param context   the context for post-processing the response
278      *
279      * @throws IOException in case of an I/O error.
280      * @throws HttpException in case of HTTP protocol violation or a processing
281      *   problem.
282      */
283     public void postProcess(
284             final ClassicHttpResponse response,
285             final HttpProcessor processor,
286             final HttpContext context) throws HttpException, IOException {
287         Args.notNull(response, "HTTP response");
288         Args.notNull(processor, "HTTP processor");
289         Args.notNull(context, "HTTP context");
290         final ProtocolVersion transportVersion = response.getVersion();
291         if (transportVersion != null) {
292             if (transportVersion.greaterEquals(HttpVersion.HTTP_2)) {
293                 throw new UnsupportedHttpVersionException(transportVersion);
294             }
295             context.setProtocolVersion(transportVersion);
296         }
297         context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
298         processor.process(response, response.getEntity(), context);
299     }
300 
301     /**
302      * Determines whether the connection can be kept alive and is safe to be re-used for subsequent message exchanges.
303      *
304      * @param request current request object.
305      * @param response  current response object.
306      * @param connection actual connection.
307      * @param context current context.
308      * @return {@code true} is the connection can be kept-alive and re-used.
309      * @throws IOException in case of an I/O error.
310      */
311     public boolean keepAlive(
312             final ClassicHttpRequest request,
313             final ClassicHttpResponse response,
314             final HttpClientConnection connection,
315             final HttpContext context) throws IOException {
316         Args.notNull(connection, "HTTP connection");
317         Args.notNull(request, "HTTP request");
318         Args.notNull(response, "HTTP response");
319         Args.notNull(context, "HTTP context");
320         final boolean keepAlive = connection.isConsistent() && connReuseStrategy.keepAlive(request, response, context);
321         if (streamListener != null) {
322             streamListener.onExchangeComplete(connection, keepAlive);
323         }
324         return keepAlive;
325     }
326 
327     /**
328      * Create a new {@link Builder}.
329      *
330      * @since 5.2
331      */
332     public static Builder builder() {
333         return new Builder();
334     }
335 
336     /**
337      * Builder for {@link HttpRequestExecutor}.
338      *
339      * @since 5.2
340      */
341     public static final class Builder {
342 
343         private Timeout waitForContinue;
344         private ConnectionReuseStrategy connReuseStrategy;
345         private Http1StreamListener streamListener;
346 
347         private Builder() {}
348 
349         public Builder withWaitForContinue(final Timeout waitForContinue) {
350             this.waitForContinue = waitForContinue;
351             return this;
352         }
353 
354         public Builder withConnectionReuseStrategy(final ConnectionReuseStrategy connReuseStrategy) {
355             this.connReuseStrategy = connReuseStrategy;
356             return this;
357         }
358 
359         public Builder withHttp1StreamListener(final Http1StreamListener streamListener) {
360             this.streamListener = streamListener;
361             return this;
362         }
363         /**
364          * Create a new HTTP Request Executor.
365          *
366          * @since 5.2
367          */
368         public HttpRequestExecutor build() {
369             return new HttpRequestExecutor(
370                     waitForContinue,
371                     connReuseStrategy,
372                     streamListener);
373         }
374     }
375 
376 }