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  package org.apache.hc.client5.http.impl.cache;
28  
29  import java.io.InputStream;
30  import java.time.Duration;
31  import java.time.Instant;
32  import java.util.Collection;
33  import java.util.Iterator;
34  import java.util.Objects;
35  import java.util.Random;
36  import java.util.concurrent.CountDownLatch;
37  import java.util.function.Consumer;
38  
39  import org.apache.hc.client5.http.cache.HttpCacheEntry;
40  import org.apache.hc.client5.http.cache.Resource;
41  import org.apache.hc.client5.http.utils.DateUtils;
42  import org.apache.hc.core5.concurrent.FutureCallback;
43  import org.apache.hc.core5.http.ClassicHttpRequest;
44  import org.apache.hc.core5.http.ClassicHttpResponse;
45  import org.apache.hc.core5.http.Header;
46  import org.apache.hc.core5.http.HttpEntity;
47  import org.apache.hc.core5.http.HttpMessage;
48  import org.apache.hc.core5.http.HttpRequest;
49  import org.apache.hc.core5.http.HttpResponse;
50  import org.apache.hc.core5.http.HttpStatus;
51  import org.apache.hc.core5.http.HttpVersion;
52  import org.apache.hc.core5.http.Method;
53  import org.apache.hc.core5.http.ProtocolVersion;
54  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
55  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
56  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
57  import org.apache.hc.core5.http.message.BasicHeader;
58  import org.apache.hc.core5.http.message.HeaderGroup;
59  import org.apache.hc.core5.http.message.MessageSupport;
60  import org.apache.hc.core5.util.ByteArrayBuffer;
61  import org.junit.jupiter.api.Assertions;
62  
63  public class HttpTestUtils {
64  
65      /*
66       * Assertions.asserts that two request or response bodies are byte-equivalent.
67       */
68      public static boolean equivalent(final HttpEntity e1, final HttpEntity e2) throws Exception {
69          final InputStream i1 = e1.getContent();
70          final InputStream i2 = e2.getContent();
71          if (i1 == null && i2 == null) {
72              return true;
73          }
74          if (i1 == null || i2 == null) {
75              return false; // avoid possible NPEs below
76          }
77          int b1 = -1;
78          while ((b1 = i1.read()) != -1) {
79              if (b1 != i2.read()) {
80                  return false;
81              }
82          }
83          return (-1 == i2.read());
84      }
85  
86      /*
87       * Retrieves the full header value by combining multiple headers and
88       * separating with commas, canonicalizing whitespace along the way.
89       */
90      public static String getCanonicalHeaderValue(final HttpMessage r, final String name) {
91          final int n = r.countHeaders(name);
92          r.getFirstHeader(name);
93          if (n == 0) {
94              return null;
95          } else if (n == 1) {
96              final Header h = r.getFirstHeader(name);
97              return h != null ? h.getValue() : null;
98          } else {
99              final StringBuilder buf = new StringBuilder();
100             for (final Iterator<Header> it = r.headerIterator(name); it.hasNext(); ) {
101                 if (buf.length() > 0) {
102                     buf.append(", ");
103                 }
104                 final Header header = it.next();
105                 if (header != null) {
106                     buf.append(header.getValue().trim());
107                 }
108             }
109             return buf.toString();
110         }
111     }
112 
113     /*
114      * Assertions.asserts that all the headers appearing in r1 also appear in r2
115      * with the same canonical header values.
116      */
117     public static boolean isEndToEndHeaderSubset(final HttpMessage r1, final HttpMessage r2) {
118         for (final Header h : r1.getHeaders()) {
119             if (!MessageSupport.isHopByHop(h.getName())) {
120                 final String r1val = getCanonicalHeaderValue(r1, h.getName());
121                 final String r2val = getCanonicalHeaderValue(r2, h.getName());
122                 if (!Objects.equals(r1val, r2val)) {
123                     return false;
124                 }
125             }
126         }
127         return true;
128     }
129 
130     /*
131      * Assertions.asserts that message {@code r2} represents exactly the same
132      * message as {@code r1}, except for hop-by-hop headers. "When a cache
133      * is semantically transparent, the client receives exactly the same
134      * response (except for hop-by-hop headers) that it would have received had
135      * its request been handled directly by the origin server."
136      */
137     public static boolean semanticallyTransparent(
138             final ClassicHttpResponse r1, final ClassicHttpResponse r2) throws Exception {
139         final boolean statusLineEquivalent = Objects.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
140                 && r1.getCode() == r2.getCode();
141         if (!statusLineEquivalent) {
142             return false;
143         }
144         final boolean headerEquivalent = isEndToEndHeaderSubset(r1, r2);
145         if (!headerEquivalent) {
146             return false;
147         }
148         final boolean entityEquivalent = equivalent(r1.getEntity(), r2.getEntity());
149         if (!entityEquivalent) {
150             return false;
151         }
152         return true;
153     }
154 
155     /* Assertions.asserts that protocol versions equivalent. */
156     public static boolean equivalent(final ProtocolVersion v1, final ProtocolVersion v2) {
157         return Objects.equals(v1 != null ? v1 : HttpVersion.DEFAULT, v2 != null ? v2 : HttpVersion.DEFAULT );
158     }
159 
160     /* Assertions.asserts that two requests are morally equivalent. */
161     public static boolean equivalent(final HttpRequest r1, final HttpRequest r2) {
162         return equivalent(r1.getVersion(), r2.getVersion()) &&
163                 Objects.equals(r1.getMethod(), r2.getMethod()) &&
164                 Objects.equals(r1.getRequestUri(), r2.getRequestUri()) &&
165                 isEndToEndHeaderSubset(r1, r2);
166     }
167 
168     /* Assertions.asserts that two requests are morally equivalent. */
169     public static boolean equivalent(final HttpResponse r1, final HttpResponse r2) {
170         return equivalent(r1.getVersion(), r2.getVersion()) &&
171                 r1.getCode() == r2.getCode() &&
172                 Objects.equals(r1.getReasonPhrase(), r2.getReasonPhrase()) &&
173                 isEndToEndHeaderSubset(r1, r2);
174     }
175 
176     public static byte[] makeRandomBytes(final int nbytes) {
177         final byte[] bytes = new byte[nbytes];
178         new Random().nextBytes(bytes);
179         return bytes;
180     }
181 
182     public static Resource makeRandomResource(final int nbytes) {
183         final byte[] bytes = new byte[nbytes];
184         new Random().nextBytes(bytes);
185         return new HeapResource(bytes);
186     }
187 
188     public static Resource makeNullResource() {
189         return null;
190     }
191 
192     public static ByteArrayBuffer makeRandomBuffer(final int nbytes) {
193         final ByteArrayBuffer buf = new ByteArrayBuffer(nbytes);
194         buf.setLength(nbytes);
195         new Random().nextBytes(buf.array());
196         return buf;
197     }
198 
199     /** Generates a response body with random content.
200      *  @param nbytes length of the desired response body
201      *  @return an {@link HttpEntity}
202      */
203     public static HttpEntity makeBody(final int nbytes) {
204         return new ByteArrayEntity(makeRandomBytes(nbytes), null);
205     }
206 
207     public static HeaderGroup headers(final Header... headers) {
208         final HeaderGroup headerGroup = new HeaderGroup();
209         if (headers != null && headers.length > 0) {
210             headerGroup.setHeaders(headers);
211         }
212         return headerGroup;
213     }
214 
215     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
216                                                 final Instant responseDate,
217                                                 final Method method,
218                                                 final String requestUri,
219                                                 final Header[] requestHeaders,
220                                                 final int status,
221                                                 final Header[] responseHeaders,
222                                                 final Collection<String> variants) {
223         return new HttpCacheEntry(
224                 requestDate,
225                 responseDate,
226                 method.name(), requestUri, headers(requestHeaders),
227                 status, headers(responseHeaders),
228                 null,
229                 variants);
230     }
231 
232     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
233                                                 final Instant responseDate,
234                                                 final Method method,
235                                                 final String requestUri,
236                                                 final Header[] requestHeaders,
237                                                 final int status,
238                                                 final Header[] responseHeaders,
239                                                 final Resource resource) {
240         return new HttpCacheEntry(
241                 requestDate,
242                 responseDate,
243                 method.name(), requestUri, headers(requestHeaders),
244                 status, headers(responseHeaders),
245                 resource,
246                 null);
247     }
248 
249     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
250                                                 final Instant responseDate,
251                                                 final int status,
252                                                 final Header[] responseHeaders,
253                                                 final Collection<String> variants) {
254         return makeCacheEntry(
255                 requestDate,
256                 responseDate,
257                 Method.GET, "/", null,
258                 status, responseHeaders,
259                 variants);
260     }
261 
262     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
263                                                 final Instant responseDate,
264                                                 final int status,
265                                                 final Header[] responseHeaders,
266                                                 final Resource resource) {
267         return makeCacheEntry(
268                 requestDate,
269                 responseDate,
270                 Method.GET, "/", null,
271                 status, responseHeaders,
272                 resource);
273     }
274 
275     public static Header[] getStockHeaders(final Instant when) {
276         return new Header[] {
277                 new BasicHeader("Date", DateUtils.formatStandardDate(when)),
278                 new BasicHeader("Server", "MockServer/1.0")
279         };
280     }
281 
282     public static HttpCacheEntry makeCacheEntry(final Instant requestDate, final Instant responseDate) {
283         final Duration diff = Duration.between(requestDate, responseDate);
284         final Instant when = requestDate.plusMillis(diff.toMillis() / 2);
285         return makeCacheEntry(requestDate, responseDate, getStockHeaders(when));
286     }
287 
288     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
289                                                 final Instant responseDate,
290                                                 final Header[] headers,
291                                                 final byte[] bytes) {
292         return makeCacheEntry(requestDate, responseDate, HttpStatus.SC_OK, headers, new HeapResource(bytes));
293     }
294 
295     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
296                                                 final Instant responseDate,
297                                                 final Header... headers) {
298         return makeCacheEntry(requestDate, responseDate, headers, makeRandomBytes(128));
299     }
300 
301     public static HttpCacheEntry makeCacheEntry(final Instant requestDate,
302                                                 final Instant responseDate,
303                                                 final Header[] headers,
304                                                 final Collection<String> variants) {
305         return makeCacheEntry(requestDate, responseDate, Method.GET, "/", null, HttpStatus.SC_OK, headers, variants);
306     }
307 
308     public static HttpCacheEntry makeCacheEntry(final Collection<String> variants) {
309         final Instant now = Instant.now();
310         return makeCacheEntry(now, now, new Header[] {}, variants);
311     }
312 
313     public static HttpCacheEntry makeCacheEntry(final Header[] headers, final byte[] bytes) {
314         final Instant now = Instant.now();
315         return makeCacheEntry(now, now, headers, bytes);
316     }
317 
318     public static HttpCacheEntry makeCacheEntry(final byte[] bytes) {
319         final Instant now = Instant.now();
320         return makeCacheEntry(getStockHeaders(now), bytes);
321     }
322 
323     public static HttpCacheEntry makeCacheEntry(final Header... headers) {
324         return makeCacheEntry(headers, makeRandomBytes(128));
325     }
326 
327     public static HttpCacheEntry makeCacheEntry() {
328         final Instant now = Instant.now();
329         return makeCacheEntry(now, now);
330     }
331 
332     public static ClassicHttpResponse make200Response() {
333         final ClassicHttpResponse out = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
334         out.setHeader("Date", DateUtils.formatStandardDate(Instant.now()));
335         out.setHeader("Server", "MockOrigin/1.0");
336         out.setHeader("Content-Length", "128");
337         out.setEntity(makeBody(128));
338         return out;
339     }
340 
341     public static final ClassicHttpResponse make200Response(final Instant date, final String cacheControl) {
342         final ClassicHttpResponse response = HttpTestUtils.make200Response();
343         response.setHeader("Date", DateUtils.formatStandardDate(date));
344         response.setHeader("Cache-Control",cacheControl);
345         response.setHeader("Etag","\"etag\"");
346         return response;
347     }
348 
349     public static ClassicHttpResponse make304Response() {
350         return new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not modified");
351     }
352 
353     public static ClassicHttpRequest makeDefaultRequest() {
354         return new BasicClassicHttpRequest(Method.GET.toString(), "/");
355     }
356 
357     public static ClassicHttpRequest makeDefaultHEADRequest() {
358         return new BasicClassicHttpRequest(Method.HEAD.toString(), "/");
359     }
360 
361     public static ClassicHttpResponse make500Response() {
362         return new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
363     }
364 
365     public static <T> FutureCallback<T> countDown(final CountDownLatch latch, final Consumer<T> consumer) {
366         return new FutureCallback<T>() {
367 
368             @Override
369             public void completed(final T result) {
370                 if (consumer != null) {
371                     consumer.accept(result);
372                 }
373                 latch.countDown();
374             }
375 
376             @Override
377             public void failed(final Exception ex) {
378                 latch.countDown();
379                 Assertions.fail(ex);
380             }
381 
382             @Override
383             public void cancelled() {
384                 latch.countDown();
385                 Assertions.fail("Unexpected cancellation");
386             }
387 
388         };
389 
390     }
391 
392     public static <T> FutureCallback<T> countDown(final CountDownLatch latch) {
393         return countDown(latch, null);
394     }
395 
396 }