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.ProtocolException;
49  import org.apache.hc.core5.http.ProtocolVersion;
50  import org.apache.hc.core5.http.UnsupportedHttpVersionException;
51  import org.apache.hc.core5.http.config.Http1Config;
52  import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
53  import org.apache.hc.core5.http.io.HttpClientConnection;
54  import org.apache.hc.core5.http.io.HttpMessageParser;
55  import org.apache.hc.core5.http.io.HttpMessageParserFactory;
56  import org.apache.hc.core5.http.io.HttpMessageWriter;
57  import org.apache.hc.core5.http.io.HttpMessageWriterFactory;
58  import org.apache.hc.core5.http.io.ResponseOutOfOrderStrategy;
59  import org.apache.hc.core5.http.message.BasicTokenIterator;
60  import org.apache.hc.core5.util.Args;
61  
62  /**
63   * Default implementation of {@link HttpClientConnection}.
64   *
65   * @since 4.3
66   */
67  public class DefaultBHttpClientConnection extends BHttpConnectionBase
68                                                     implements HttpClientConnection {
69  
70      private final HttpMessageParser<ClassicHttpResponse> responseParser;
71      private final HttpMessageWriter<ClassicHttpRequest> requestWriter;
72      private final ContentLengthStrategy incomingContentStrategy;
73      private final ContentLengthStrategy outgoingContentStrategy;
74      private final ResponseOutOfOrderStrategy responseOutOfOrderStrategy;
75      private volatile boolean consistent;
76  
77      /**
78       * Creates new instance of DefaultBHttpClientConnection.
79       *
80       * @param http1Config Message http1Config. If {@code null}
81       *   {@link Http1Config#DEFAULT} will be used.
82       * @param charDecoder decoder to be used for decoding HTTP protocol elements.
83       *   If {@code null} simple type cast will be used for byte to char conversion.
84       * @param charEncoder encoder to be used for encoding HTTP protocol elements.
85       *   If {@code null} simple type cast will be used for char to byte conversion.
86       * @param incomingContentStrategy incoming content length strategy. If {@code null}
87       *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
88       * @param outgoingContentStrategy outgoing content length strategy. If {@code null}
89       *   {@link DefaultContentLengthStrategy#INSTANCE} will be used.
90       * @param responseOutOfOrderStrategy response out of order strategy. If {@code null}
91       *   {@link NoResponseOutOfOrderStrategy#INSTANCE} will be used.
92       * @param requestWriterFactory request writer factory. If {@code null}
93       *   {@link DefaultHttpRequestWriterFactory#INSTANCE} will be used.
94       * @param responseParserFactory response parser factory. If {@code null}
95       *   {@link DefaultHttpResponseParserFactory#INSTANCE} will be used.
96       */
97      public DefaultBHttpClientConnection(
98              final Http1Config http1Config,
99              final CharsetDecoder charDecoder,
100             final CharsetEncoder charEncoder,
101             final ContentLengthStrategy incomingContentStrategy,
102             final ContentLengthStrategy outgoingContentStrategy,
103             final ResponseOutOfOrderStrategy responseOutOfOrderStrategy,
104             final HttpMessageWriterFactory<ClassicHttpRequest> requestWriterFactory,
105             final HttpMessageParserFactory<ClassicHttpResponse> responseParserFactory) {
106         super(http1Config, charDecoder, charEncoder);
107         this.requestWriter = (requestWriterFactory != null ? requestWriterFactory :
108             DefaultHttpRequestWriterFactory.INSTANCE).create();
109         this.responseParser = (responseParserFactory != null ? responseParserFactory :
110             DefaultHttpResponseParserFactory.INSTANCE).create(http1Config);
111         this.incomingContentStrategy = incomingContentStrategy != null ? incomingContentStrategy :
112             DefaultContentLengthStrategy.INSTANCE;
113         this.outgoingContentStrategy = outgoingContentStrategy != null ? outgoingContentStrategy :
114             DefaultContentLengthStrategy.INSTANCE;
115         this.responseOutOfOrderStrategy = responseOutOfOrderStrategy != null ? responseOutOfOrderStrategy :
116             NoResponseOutOfOrderStrategy.INSTANCE;
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                         checkForEarlyResponse(totalBytes, b.length);
223                         totalBytes += b.length;
224                         socketOutputStream.write(b);
225                     }
226 
227                     @Override
228                     public void write(final byte[] b, final int off, final int len) throws IOException {
229                         checkForEarlyResponse(totalBytes, len);
230                         totalBytes += len;
231                         socketOutputStream.write(b, off, len);
232                     }
233 
234                     @Override
235                     public void write(final int b) throws IOException {
236                         checkForEarlyResponse(totalBytes, 1);
237                         totalBytes++;
238                         socketOutputStream.write(b);
239                     }
240 
241                     @Override
242                     public void flush() throws IOException {
243                         socketOutputStream.flush();
244                     }
245 
246                     @Override
247                     public void close() throws IOException {
248                         socketOutputStream.close();
249                     }
250 
251                 }, entity.getTrailers())) {
252             entity.writeTo(outStream);
253         } catch (final ResponseOutOfOrderException ex) {
254             if (len > 0) {
255                 this.consistent = false;
256             }
257         }
258     }
259 
260     @Override
261     public boolean isConsistent() {
262         return this.consistent;
263     }
264 
265     @Override
266     public void terminateRequest(final ClassicHttpRequest request) throws HttpException, IOException {
267         Args.notNull(request, "HTTP request");
268         final SocketHolder socketHolder = ensureOpen();
269         final HttpEntity entity = request.getEntity();
270         if (entity == null) {
271             return;
272         }
273         final Iterator<String> ti = new BasicTokenIterator(request.headerIterator(HttpHeaders.CONNECTION));
274         while (ti.hasNext()) {
275             final String token = ti.next();
276             if (HeaderElements.CLOSE.equalsIgnoreCase(token)) {
277                 this.consistent = false;
278                 return;
279             }
280         }
281         final long len = this.outgoingContentStrategy.determineLength(request);
282         if (len == ContentLengthStrategy.CHUNKED) {
283             try (final OutputStream outStream = createContentOutputStream(len, this.outbuffer, socketHolder.getOutputStream(), entity.getTrailers())) {
284                 // just close
285             }
286         } else if (len >= 0 && len <= 1024) {
287             try (final OutputStream outStream = createContentOutputStream(len, this.outbuffer, socketHolder.getOutputStream(), null)) {
288                 entity.writeTo(outStream);
289             }
290         } else {
291             this.consistent = false;
292         }
293     }
294 
295     @Override
296     public ClassicHttpResponse receiveResponseHeader() throws HttpException, IOException {
297         final SocketHolder socketHolder = ensureOpen();
298         final ClassicHttpResponse response = this.responseParser.parse(this.inBuffer, socketHolder.getInputStream());
299         final ProtocolVersion transportVersion = response.getVersion();
300         if (transportVersion != null && transportVersion.greaterEquals(HttpVersion.HTTP_2)) {
301             throw new UnsupportedHttpVersionException(transportVersion);
302         }
303         this.version = transportVersion;
304         onResponseReceived(response);
305         final int status = response.getCode();
306         if (status < HttpStatus.SC_INFORMATIONAL) {
307             throw new ProtocolException("Invalid response: " + status);
308         }
309         if (response.getCode() >= HttpStatus.SC_SUCCESS) {
310             incrementResponseCount();
311         }
312         return response;
313     }
314 
315     @Override
316     public void receiveResponseEntity( final ClassicHttpResponse response) throws HttpException, IOException {
317         Args.notNull(response, "HTTP response");
318         final SocketHolder socketHolder = ensureOpen();
319         final long len = this.incomingContentStrategy.determineLength(response);
320         response.setEntity(createIncomingEntity(response, this.inBuffer, socketHolder.getInputStream(), len));
321     }
322 }