View Javadoc
1   /*
2    * Copyright (c) 2011-2022, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.http.request;
31  
32  import com.jcabi.aspects.Immutable;
33  import com.jcabi.aspects.Loggable;
34  import com.jcabi.http.ImmutableHeader;
35  import com.jcabi.http.Request;
36  import com.jcabi.http.RequestBody;
37  import com.jcabi.http.RequestURI;
38  import com.jcabi.http.Response;
39  import com.jcabi.http.Wire;
40  import com.jcabi.immutable.Array;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.net.URI;
44  import java.net.URL;
45  import java.util.Collection;
46  import java.util.LinkedList;
47  import java.util.Map;
48  import lombok.EqualsAndHashCode;
49  import lombok.ToString;
50  import org.apache.http.Header;
51  import org.apache.http.HttpEntity;
52  import org.apache.http.client.config.RequestConfig;
53  import org.apache.http.client.methods.CloseableHttpResponse;
54  import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
55  import org.apache.http.entity.BufferedHttpEntity;
56  import org.apache.http.entity.InputStreamEntity;
57  import org.apache.http.impl.client.HttpClients;
58  import org.apache.http.util.EntityUtils;
59  
60  /**
61   * Implementation of {@link Request},
62   * based on Apache HTTP client.
63   *
64   * <p>The class is immutable and thread-safe.
65   *
66   * @since 0.8
67   * @checkstyle ClassDataAbstractionCoupling (500 lines)
68   * @todo #200:30m TrustedWire does not support ApacheRequest.
69   *  Investigate if it's possible for them to work together,
70   *  if not see jcabi-http#178 for discussion about alternative solutions.
71   */
72  @Immutable
73  @EqualsAndHashCode(of = "base")
74  @ToString(of = "base")
75  @Loggable(Loggable.DEBUG)
76  @SuppressWarnings("PMD.TooManyMethods")
77  public final class ApacheRequest implements Request {
78  
79      /**
80       * The wire to use.
81       * @checkstyle AnonInnerLength (200 lines)
82       */
83      private static final Wire WIRE = new Wire() {
84          // @checkstyle ParameterNumber (6 lines)
85          @Override
86          public Response send(final Request req, final String home,
87              final String method,
88              final Collection<Map.Entry<String, String>> headers,
89              final InputStream content,
90              final int connect,
91              final int read) throws IOException {
92              final CloseableHttpResponse response =
93                  HttpClients.createSystem().execute(
94                      this.httpRequest(
95                          home, method, headers, content,
96                          connect, read
97                      )
98                  );
99              try {
100                 return new DefaultResponse(
101                     req,
102                     response.getStatusLine().getStatusCode(),
103                     response.getStatusLine().getReasonPhrase(),
104                     this.headers(response.getAllHeaders()),
105                     this.consume(response.getEntity())
106                 );
107             } finally {
108                 response.close();
109             }
110         }
111 
112         /**
113          * Create request.
114          * @param home Home URI
115          * @param method Method to use
116          * @param headers HTTP Headers to use
117          * @param content Content to send
118          * @param connect Connect timeout
119          * @param read Read timeout
120          * @return Request
121          * @throws IOException If an IO Exception occurs
122          * @checkstyle ParameterNumber (6 lines)
123          */
124         public HttpEntityEnclosingRequestBase httpRequest(final String home,
125             final String method,
126             final Collection<Map.Entry<String, String>> headers,
127             final InputStream content,
128             final int connect,
129             final int read) throws IOException {
130             final HttpEntityEnclosingRequestBase req =
131                 new HttpEntityEnclosingRequestBase() {
132                     @Override
133                     public String getMethod() {
134                         return method;
135                     }
136                 };
137             final URI uri = URI.create(home);
138             req.setConfig(
139                 RequestConfig.custom()
140                     .setCircularRedirectsAllowed(false)
141                     .setRedirectsEnabled(false)
142                     .setConnectTimeout(connect)
143                     .setSocketTimeout(read)
144                     .build()
145             );
146             req.setURI(uri);
147             req.setEntity(
148                 new BufferedHttpEntity(new InputStreamEntity(content))
149             );
150             for (final Map.Entry<String, String> header : headers) {
151                 req.addHeader(header.getKey(), header.getValue());
152             }
153             return req;
154         }
155 
156         /**
157          * Fetch body from http entity.
158          * @param entity HTTP entity
159          * @return Body in UTF-8
160          * @throws IOException If fails
161          */
162         private byte[] consume(final HttpEntity entity) throws IOException {
163             final byte[] body;
164             if (entity == null) {
165                 body = new byte[0];
166             } else {
167                 body = EntityUtils.toByteArray(entity);
168             }
169             return body;
170         }
171 
172         /**
173          * Make a list of all hdrs.
174          * @param list Apache HTTP hdrs
175          * @return Body in UTF-8
176          */
177         @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
178         private Array<Map.Entry<String, String>> headers(final Header... list) {
179             final Collection<Map.Entry<String, String>> headers =
180                 new LinkedList<>();
181             for (final Header header : list) {
182                 headers.add(
183                     new ImmutableHeader(
184                         header.getName(),
185                         header.getValue()
186                     )
187                 );
188             }
189             return new Array<Map.Entry<String, String>>(headers);
190         }
191     };
192 
193     /**
194      * Base request.
195      */
196     private final transient Request base;
197 
198     /**
199      * Public ctor.
200      * @param url The resource to work with
201      */
202     public ApacheRequest(final URL url) {
203         this(url.toString());
204     }
205 
206     /**
207      * Public ctor.
208      * @param uri The resource to work with
209      */
210     public ApacheRequest(final URI uri) {
211         this(uri.toString());
212     }
213 
214     /**
215      * Public ctor.
216      * @param uri The resource to work with
217      */
218     public ApacheRequest(final String uri) {
219         this.base = new BaseRequest(ApacheRequest.WIRE, uri);
220     }
221 
222     @Override
223     public RequestURI uri() {
224         return this.base.uri();
225     }
226 
227     @Override
228     public Request header(final String name, final Object value) {
229         return this.base.header(name, value);
230     }
231 
232     @Override
233     public Request reset(final String name) {
234         return this.base.reset(name);
235     }
236 
237     @Override
238     public RequestBody body() {
239         return this.base.body();
240     }
241 
242     @Override
243     public RequestBody multipartBody() {
244         return this.base.multipartBody();
245     }
246 
247     @Override
248     public Request method(final String method) {
249         return this.base.method(method);
250     }
251 
252     @Override
253     public Request timeout(final int connect, final int read) {
254         return this.base.timeout(connect, read);
255     }
256 
257     @Override
258     public Response fetch() throws IOException {
259         return this.base.fetch();
260     }
261 
262     @Override
263     public Response fetch(final InputStream stream) throws IOException {
264         return this.base.fetch(stream);
265     }
266 
267     @Override
268     public <T extends Wire> Request through(final Class<T> type,
269         final Object... args) {
270         return this.base.through(type, args);
271     }
272 
273     @Override
274     public Request through(final Wire wire) {
275         return this.base.through(wire);
276     }
277 }