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  import java.io.InputStream;
32  import java.io.OutputStream;
33  import java.net.Socket;
34  import java.nio.charset.CharsetDecoder;
35  import java.nio.charset.CharsetEncoder;
36  import java.util.Iterator;
37  
38  import org.apache.hc.core5.http.ClassicHttpRequest;
39  import org.apache.hc.core5.http.ClassicHttpResponse;
40  import org.apache.hc.core5.http.ContentLengthStrategy;
41  import org.apache.hc.core5.http.HeaderElements;
42  import org.apache.hc.core5.http.HttpEntity;
43  import org.apache.hc.core5.http.HttpException;
44  import org.apache.hc.core5.http.HttpHeaders;
45  import org.apache.hc.core5.http.HttpStatus;
46  import org.apache.hc.core5.http.HttpVersion;
47  import org.apache.hc.core5.http.LengthRequiredException;
48  import org.apache.hc.core5.http.NoHttpResponseException;
49  import org.apache.hc.core5.http.ProtocolException;
50  import org.apache.hc.core5.http.ProtocolVersion;
51  import org.apache.hc.core5.http.UnsupportedHttpVersionException;
52  import org.apache.hc.core5.http.config.Http1Config;
53  import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
54  import org.apache.hc.core5.http.io.HttpClientConnection;
55  import org.apache.hc.core5.http.io.HttpMessageParser;
56  import org.apache.hc.core5.http.io.HttpMessageParserFactory;
57  import org.apache.hc.core5.http.io.HttpMessageWriter;
58  import org.apache.hc.core5.http.io.HttpMessageWriterFactory;
59  import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
60  import org.apache.hc.core5.http.message.BasicTokenIterator;
61  import org.apache.hc.core5.util.Args;
62  
63  /**
64   * Default implementation of {@link HttpClientConnection}.
65   *
66   * @since 4.3
67   */
68  public class DefaultBHttpClientConnection extends BHttpConnectionBase
69                                                     implements HttpClientConnection {
70  
71      private final HttpMessageParser<ClassicHttpResponse> responseParser;
72      private final HttpMessageWriter<ClassicHttpRequest> requestWriter;
73      private final ContentLengthStrategy incomingContentStrategy;
74      private final ContentLengthStrategy outgoingContentStrategy;
75      private final ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
76      private volatile boolean consistent;
77  
78      /**
79       * Creates new instance of DefaultBHttpClientConnection.
80       *
81       * @param http1Config Message http1Config. If {@code null}
82       *   {@link Http1Config#DEFAULT} will be used.
83       * @param charDecoder decoder to be used for decoding HTTP protocol elements.
84       *   If {@code null} simple type cast will be used for byte to char conversion.
85       * @param charEncoder encoder to be used for encoding HTTP protocol elements.
86       *   If {@code null} simple type cast will be used for char to byte conversion.
87       * @param incomingContentStrategy incoming content length strategy. If {@code null}
88       *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
89       * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
90       *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
91       * @param responseOutOfOrderStrategy response out of order strategy. If {@code null}
92       *   {@link NoResponseOutOfOrderStrategy#INSTANCE} will be used.
93       * @param requestWriterFactory request writer factory. If {@code null}
94       *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
95       * @param responseParserFactory response parser factory. If {@code null}
96       *   {@link DefaultHttpResponseParserFactory#INSTANCE} will be used.
97       */
98      public DefaultBHttpClientConnection(
99              final Http1Config http1Config,
100             final CharsetDecoder charDecoder,
101             final CharsetEncoder charEncoder,
102             final ContentLengthStrategy incomingContentStrategy,
103             final ContentLengthStrategy outgoingContentStrategy,
104             final ResponseOutOfOrderStrategy responseOutOfOrderStrategy,
105             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
106             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
107         super(http1Config, charDecoder, charEncoder);
108         this.requestWriter = (requestWriterFactory != null ? requestWriterFactory :
109             DefaultHttpRequestWriterFactory.INSTANCE).create();
110         this.responseParser = (responseParserFactory != null ? responseParserFactory :
111             DefaultHttpResponseParserFactory.INSTANCE).create(http1Config);
112         this.incomingContentStrategy = incomingContentStrategy != null ? incomingContentStrategy :
113             DefaultContentLengthStrategy.INSTANCE;
114         this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy :
115             DefaultContentLengthStrategy.INSTANCE;
116         this.responseOutOfOrderStrategy = responseOutOfOrderStrategy;
117         this.consistent = true;
118     }
119 
120     /**
121      * Creates new instance of DefaultBHttpClientConnection.
122      *
123      * @param http1Config Message http1Config. If {@code null}
124      *   {@link Http1Config#DEFAULT} will be used.
125      * @param charDecoder decoder to be used for decoding HTTP protocol elements.
126      *   If {@code null} simple type cast will be used for byte to char conversion.
127      * @param charEncoder encoder to be used for encoding HTTP protocol elements.
128      *   If {@code null} simple type cast will be used for char to byte conversion.
129      * @param incomingContentStrategy incoming content length strategy. If {@code null}
130      *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
131      * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
132      *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
133      * @param requestWriterFactory request writer factory. If {@code null}
134      *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
135      * @param responseParserFactory response parser factory. If {@code null}
136      *   {@link DefaultHttpResponseParserFactory#INSTANCE} will be used.
137      */
138     public DefaultBHttpClientConnection(
139             final Http1Config http1Config,
140             final CharsetDecoder charDecoder,
141             final CharsetEncoder charEncoder,
142             final ContentLengthStrategy incomingContentStrategy,
143             final ContentLengthStrategy outgoingContentStrategy,
144             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
145             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
146         this(
147                 http1Config,
148                 charDecoder,
149                 charEncoder,
150                 incomingContentStrategy,
151                 outgoingContentStrategy,
152                 null,
153                 requestWriterFactory,
154                 responseParserFactory);
155     }
156 
157     public DefaultBHttpClientConnection(
158             final Http1Config http1Config,
159             final CharsetDecoder charDecoder,
160             final CharsetEncoder charEncoder) {
161         this(http1Config, charDecoder, charEncoder, null, null, null, null);
162     }
163 
164     public DefaultBHttpClientConnection(final Http1Config http1Config) {
165         this(http1Config, null, null);
166     }
167 
168     protected void onResponseReceived(final ClassicHttpResponse response) {
169     }
170 
171     protected void onRequestSubmitted(final ClassicHttpRequest request) {
172     }
173 
174     @Override
175     public void bind(final Socket socket) throws IOException {
176         super.bind(socket);
177     }
178 
179     @Override
180     public void sendRequestHeader(final ClassicHttpRequest request)
181             throws HttpException, IOException {
182         Args.notNull(request, "HTTP request");
183         final SocketHolder socketHolder = ensureOpen();
184         this.requestWriter.write(request, this.outbuffer, socketHolder.getOutputStream());
185         onRequestSubmitted(request);
186         incrementRequestCount();
187     }
188 
189     @Override
190     public void sendRequestEntity(final ClassicHttpRequest request) throws HttpException, IOException {
191         Args.notNull(request, "HTTP request");
192         final SocketHolder socketHolder = ensureOpen();
193         final HttpEntity entity = request.getEntity();
194         if (entity == null) {
195             return;
196         }
197         final long len = this.outgoingContentStrategy.determineLength(request);
198         if (len == ContentLengthStrategy.UNDEFINED) {
199             throw new LengthRequiredException();
200         }
201         try (final OutputStream outStream = createContentOutputStream(
202                 len, this.outbuffer, new OutputStream() {
203 
204                     final OutputStream socketOutputStream = socketHolder.getOutputStream();
205                     final InputStream socketInputStream = socketHolder.getInputStream();
206 
207                     long totalBytes;
208 
209                     void checkForEarlyResponse(final long totalBytesSent, final int nextWriteSize) throws IOException {
210                         if (responseOutOfOrderStrategy.isEarlyResponseDetected(
211                                 request,
212                                 DefaultBHttpClientConnection.this,
213                                 socketInputStream,
214                                 totalBytesSent,
215                                 nextWriteSize)) {
216                             throw new ResponseOutOfOrderException();
217                         }
218                     }
219 
220                     @Override
221                     public void write(final byte[] b) throws IOException {
222                         if (responseOutOfOrderStrategy != null) {
223                             checkForEarlyResponse(totalBytes, b.length);
224                         }
225                         totalBytes += b.length;
226                         socketOutputStream.write(b);
227                     }
228 
229                     @Override
230                     public void write(final byte[] b, final int off, final int len) throws IOException {
231                         if (responseOutOfOrderStrategy != null) {
232                             checkForEarlyResponse(totalBytes, len);
233                         }
234                         totalBytes += len;
235                         socketOutputStream.write(b, off, len);
236                     }
237 
238                     @Override
239                     public void write(final int b) throws IOException {
240                         if (responseOutOfOrderStrategy != null) {
241                             checkForEarlyResponse(totalBytes, 1);
242                         }
243                         totalBytes++;
244                         socketOutputStream.write(b);
245                     }
246 
247                     @Override
248                     public void flush() throws IOException {
249                         socketOutputStream.flush();
250                     }
251 
252                     @Override
253                     public void close() throws IOException {
254                         socketOutputStream.close();
255                     }
256 
257                 }, entity.getTrailers())) {
258             entity.writeTo(outStream);
259         } catch (final ResponseOutOfOrderException ex) {
260             if (len > 0) {
261                 this.consistent = false;
262             }
263         }
264     }
265 
266     @Override
267     public boolean isConsistent() {
268         return this.consistent;
269     }
270 
271     @Override
272     public void terminateRequest(final ClassicHttpRequest request) throws HttpException, IOException {
273         Args.notNull(request, "HTTP request");
274         final SocketHolder socketHolder = ensureOpen();
275         final HttpEntity entity = request.getEntity();
276         if (entity == null) {
277             return;
278         }
279         final Iterator<String> ti = new BasicTokenIterator(request.headerIterator(HttpHeaders.CONNECTION));
280         while (ti.hasNext()) {
281             final String token = ti.next();
282             if (HeaderElements.CLOSE.equalsIgnoreCase(token)) {
283                 this.consistent = false;
284                 return;
285             }
286         }
287         final long len = this.outgoingContentStrategy.determineLength(request);
288         if (len == ContentLengthStrategy.CHUNKED) {
289             try (final OutputStream outStream = createContentOutputStream(len, this.outbuffer, socketHolder.getOutputStream(), entity.getTrailers())) {
290                 // just close
291             }
292         } else if (len >= 0 && len <= 1024) {
293             try (final OutputStream outStream = createContentOutputStream(len, this.outbuffer, socketHolder.getOutputStream(), null)) {
294                 entity.writeTo(outStream);
295             }
296         } else {
297             this.consistent = false;
298         }
299     }
300 
301     @Override
302     public ClassicHttpResponse receiveResponseHeader() throws HttpException, IOException {
303         final SocketHolder socketHolder = ensureOpen();
304         final ClassicHttpResponse response = this.responseParser.parse(this.inBuffer, socketHolder.getInputStream());
305         if (response == null) {
306             throw new NoHttpResponseException("The target server failed to respond");
307         }
308         final ProtocolVersion transportVersion = response.getVersion();
309         if (transportVersion != null && transportVersion.greaterEquals(HttpVersion.HTTP_2)) {
310             throw new UnsupportedHttpVersionException(transportVersion);
311         }
312         this.version = transportVersion;
313         onResponseReceived(response);
314         final int status = response.getCode();
315         if (status < HttpStatus.SC_INFORMATIONAL) {
316             throw new ProtocolException("Invalid response: " + status);
317         }
318         if (response.getCode() >= HttpStatus.SC_SUCCESS) {
319             incrementResponseCount();
320         }
321         return response;
322     }
323 
324     @Override
325     public void receiveResponseEntity( final ClassicHttpResponse response) throws HttpException, IOException {
326         Args.notNull(response, "HTTP response");
327         final SocketHolder socketHolder = ensureOpen();
328         final long len = this.incomingContentStrategy.determineLength(response);
329         response.setEntity(createIncomingEntity(response, this.inBuffer, socketHolder.getInputStream(), len));
330     }
331 }