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.client5.http.impl.classic;
29  
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.OutputStream;
33  import java.net.SocketException;
34  import java.util.Arrays;
35  import java.util.List;
36  
37  import org.apache.hc.client5.http.classic.ExecRuntime;
38  import org.apache.hc.core5.function.Supplier;
39  import org.apache.hc.core5.http.ClassicHttpResponse;
40  import org.apache.hc.core5.http.Header;
41  import org.apache.hc.core5.http.HttpEntity;
42  import org.apache.hc.core5.http.impl.io.ChunkedInputStream;
43  import org.apache.hc.core5.http.io.EofSensorInputStream;
44  import org.apache.hc.core5.http.io.EofSensorWatcher;
45  import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
46  
47  class ResponseEntityProxy extends HttpEntityWrapper implements EofSensorWatcher {
48  
49      private final ExecRuntime execRuntime;
50  
51      public static void enhance(final ClassicHttpResponse response, final ExecRuntime execRuntime) {
52          final HttpEntity entity = response.getEntity();
53          if (entity != null && entity.isStreaming() && execRuntime != null) {
54              response.setEntity(new ResponseEntityProxy(entity, execRuntime));
55          }
56      }
57  
58      ResponseEntityProxy(final HttpEntity entity, final ExecRuntime execRuntime) {
59          super(entity);
60          this.execRuntime = execRuntime;
61      }
62  
63      private void cleanup() throws IOException {
64          if (this.execRuntime != null) {
65              if (this.execRuntime.isEndpointConnected()) {
66                  this.execRuntime.disconnectEndpoint();
67              }
68              this.execRuntime.discardEndpoint();
69          }
70      }
71  
72      private void discardConnection() {
73          if (this.execRuntime != null) {
74              this.execRuntime.discardEndpoint();
75          }
76      }
77  
78      public void releaseConnection() {
79          if (this.execRuntime != null) {
80              this.execRuntime.releaseEndpoint();
81          }
82      }
83  
84      @Override
85      public boolean isRepeatable() {
86          return false;
87      }
88  
89      @Override
90      public InputStream getContent() throws IOException {
91          return new EofSensorInputStream(super.getContent(), this);
92      }
93  
94      @Override
95      public void writeTo(final OutputStream outStream) throws IOException {
96          try {
97              super.writeTo(outStream != null ? outStream : NullOutputStream.INSTANCE);
98              releaseConnection();
99          } catch (final IOException | RuntimeException ex) {
100             discardConnection();
101             throw ex;
102         } finally {
103             cleanup();
104         }
105     }
106 
107     @Override
108     public boolean eofDetected(final InputStream wrapped) throws IOException {
109         try {
110             // there may be some cleanup required, such as
111             // reading trailers after the response body:
112             if (wrapped != null) {
113                 wrapped.close();
114             }
115             releaseConnection();
116         } catch (final IOException | RuntimeException ex) {
117             discardConnection();
118             throw ex;
119         } finally {
120             cleanup();
121         }
122         return false;
123     }
124 
125     @Override
126     public boolean streamClosed(final InputStream wrapped) throws IOException {
127         try {
128             final boolean open = execRuntime != null && execRuntime.isEndpointAcquired();
129             // this assumes that closing the stream will
130             // consume the remainder of the response body:
131             try {
132                 if (wrapped != null) {
133                     wrapped.close();
134                 }
135                 releaseConnection();
136             } catch (final SocketException ex) {
137                 if (open) {
138                     throw ex;
139                 }
140             }
141         } catch (final IOException | RuntimeException ex) {
142             discardConnection();
143             throw ex;
144         } finally {
145             cleanup();
146         }
147         return false;
148     }
149 
150     @Override
151     public boolean streamAbort(final InputStream wrapped) throws IOException {
152         cleanup();
153         return false;
154     }
155 
156     @Override
157     public Supplier<List<? extends Header>> getTrailers() {
158             try {
159                 final InputStream underlyingStream = super.getContent();
160                 return () -> {
161                     final Header[] footers;
162                     if (underlyingStream instanceof ChunkedInputStream) {
163                         final ChunkedInputStream chunkedInputStream = (ChunkedInputStream) underlyingStream;
164                         footers = chunkedInputStream.getFooters();
165                     } else {
166                         footers = new Header[0];
167                     }
168                     return Arrays.asList(footers);
169                 };
170             } catch (final IOException e) {
171                 throw new IllegalStateException("Unable to retrieve input stream", e);
172             }
173     }
174 
175     @Override
176     public void close() throws IOException {
177         try {
178             // HttpEntity.close will close the underlying resource. Closing a reusable request stream results in
179             // draining remaining data, allowing for connection reuse.
180             super.close();
181             releaseConnection();
182         } catch (final IOException | RuntimeException ex) {
183             discardConnection();
184             throw ex;
185         } finally {
186             cleanup();
187         }
188     }
189 
190     private static final class NullOutputStream extends OutputStream {
191         private static final NullOutputStream INSTANCE = new NullOutputStream();
192 
193         private NullOutputStream() {}
194 
195         @Override
196         public void write(@SuppressWarnings("unused") final int byteValue) {
197             // no-op
198         }
199 
200         @Override
201         public void write(@SuppressWarnings("unused") final byte[] buffer) {
202             // no-op
203         }
204 
205         @Override
206         public void write(
207                 @SuppressWarnings("unused") final byte[] buffer,
208                 @SuppressWarnings("unused") final int off,
209                 @SuppressWarnings("unused") final int len) {
210             // no-op
211         }
212 
213         @Override
214         public void flush() {
215             // no-op
216         }
217 
218         @Override
219         public void close() {
220             // no-op
221         }
222 
223         @Override
224         public String toString() {
225             return "NullOutputStream{}";
226         }
227     }
228 }