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.util.Date;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.Map;
34  import java.util.Random;
35  
36  import org.apache.hc.client5.http.cache.HttpCacheEntry;
37  import org.apache.hc.client5.http.utils.DateUtils;
38  import org.apache.hc.core5.http.ClassicHttpRequest;
39  import org.apache.hc.core5.http.ClassicHttpResponse;
40  import org.apache.hc.core5.http.Header;
41  import org.apache.hc.core5.http.HeaderElement;
42  import org.apache.hc.core5.http.HttpEntity;
43  import org.apache.hc.core5.http.HttpHeaders;
44  import org.apache.hc.core5.http.HttpMessage;
45  import org.apache.hc.core5.http.HttpRequest;
46  import org.apache.hc.core5.http.HttpResponse;
47  import org.apache.hc.core5.http.HttpStatus;
48  import org.apache.hc.core5.http.HttpVersion;
49  import org.apache.hc.core5.http.Method;
50  import org.apache.hc.core5.http.ProtocolVersion;
51  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
52  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
53  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
54  import org.apache.hc.core5.http.message.BasicHeader;
55  import org.apache.hc.core5.http.message.MessageSupport;
56  import org.apache.hc.core5.util.ByteArrayBuffer;
57  import org.apache.hc.core5.util.LangUtils;
58  import org.junit.Assert;
59  
60  public class HttpTestUtils {
61  
62      /*
63       * "The following HTTP/1.1 headers are hop-by-hop headers..."
64       *
65       * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
66       */
67      private static final String[] HOP_BY_HOP_HEADERS = { "Connection", "Keep-Alive", "Proxy-Authenticate",
68          "Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade" };
69  
70      /*
71       * "Multiple message-header fields with the same field-name MAY be present
72       * in a message if and only if the entire field-value for that header field
73       * is defined as a comma-separated list [i.e., #(values)]."
74       *
75       * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
76       */
77      private static final String[] MULTI_HEADERS = { "Accept", "Accept-Charset", "Accept-Encoding",
78          "Accept-Language", "Allow", "Cache-Control", "Connection", "Content-Encoding",
79          "Content-Language", "Expect", "Pragma", "Proxy-Authenticate", "TE", "Trailer",
80          "Transfer-Encoding", "Upgrade", "Via", HttpHeaders.WARNING, "WWW-Authenticate" };
81      private static final String[] SINGLE_HEADERS = { "Accept-Ranges", "Age", "Authorization",
82          "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type",
83          "Date", "ETag", "Expires", "From", "Host", "If-Match", "If-Modified-Since",
84          "If-None-Match", "If-Range", "If-Unmodified-Since", "Last-Modified", "Location",
85          "Max-Forwards", "Proxy-Authorization", "Range", "Referer", "Retry-After", "Server",
86          "User-Agent", "Vary" };
87  
88      /*
89       * Determines whether the given header name is considered a hop-by-hop
90       * header.
91       *
92       * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
93       */
94      public static boolean isHopByHopHeader(final String name) {
95          for (final String s : HOP_BY_HOP_HEADERS) {
96              if (s.equalsIgnoreCase(name)) {
97                  return true;
98              }
99          }
100         return false;
101     }
102 
103     /*
104      * Determines whether a given header name may only appear once in a message.
105      */
106     public static boolean isSingleHeader(final String name) {
107         for (final String s : SINGLE_HEADERS) {
108             if (s.equalsIgnoreCase(name)) {
109                 return true;
110             }
111         }
112         return false;
113     }
114     /*
115      * Assert.asserts that two request or response bodies are byte-equivalent.
116      */
117     public static boolean equivalent(final HttpEntity e1, final HttpEntity e2) throws Exception {
118         final InputStream i1 = e1.getContent();
119         final InputStream i2 = e2.getContent();
120         if (i1 == null && i2 == null) {
121             return true;
122         }
123         if (i1 == null || i2 == null) {
124             return false; // avoid possible NPEs below
125         }
126         int b1 = -1;
127         while ((b1 = i1.read()) != -1) {
128             if (b1 != i2.read()) {
129                 return false;
130             }
131         }
132         return (-1 == i2.read());
133     }
134 
135     /*
136      * Retrieves the full header value by combining multiple headers and
137      * separating with commas, canonicalizing whitespace along the way.
138      *
139      * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
140      */
141     public static String getCanonicalHeaderValue(final HttpMessage r, final String name) {
142         if (isSingleHeader(name)) {
143             final Header h = r.getFirstHeader(name);
144             return (h != null) ? h.getValue() : null;
145         }
146         final StringBuilder buf = new StringBuilder();
147         boolean first = true;
148         for (final Header h : r.getHeaders(name)) {
149             if (!first) {
150                 buf.append(", ");
151             }
152             buf.append(h.getValue().trim());
153             first = false;
154         }
155         return buf.toString();
156     }
157 
158     /*
159      * Assert.asserts that all the headers appearing in r1 also appear in r2
160      * with the same canonical header values.
161      */
162     public static boolean isEndToEndHeaderSubset(final HttpMessage r1, final HttpMessage r2) {
163         for (final Header h : r1.getHeaders()) {
164             if (!isHopByHopHeader(h.getName())) {
165                 final String r1val = getCanonicalHeaderValue(r1, h.getName());
166                 final String r2val = getCanonicalHeaderValue(r2, h.getName());
167                 if (!r1val.equals(r2val)) {
168                     return false;
169                 }
170             }
171         }
172         return true;
173     }
174 
175     /*
176      * Assert.asserts that message {@code r2} represents exactly the same
177      * message as {@code r1}, except for hop-by-hop headers. "When a cache
178      * is semantically transparent, the client receives exactly the same
179      * response (except for hop-by-hop headers) that it would have received had
180      * its request been handled directly by the origin server."
181      *
182      * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html#sec1.3
183      */
184     public static boolean semanticallyTransparent(
185             final ClassicHttpResponse r1, final ClassicHttpResponse r2) throws Exception {
186         final boolean entitiesEquivalent = equivalent(r1.getEntity(), r2.getEntity());
187         if (!entitiesEquivalent) {
188             return false;
189         }
190         final boolean statusLinesEquivalent = LangUtils.equals(r1.getReasonPhrase(), r2.getReasonPhrase())
191                 && r1.getCode() == r2.getCode();
192         if (!statusLinesEquivalent) {
193             return false;
194         }
195         return isEndToEndHeaderSubset(r1, r2);
196     }
197 
198     /* Assert.asserts that protocol versions equivalent. */
199     public static boolean equivalent(final ProtocolVersion v1, final ProtocolVersion v2) {
200         return LangUtils.equals(v1 != null ? v1 : HttpVersion.DEFAULT, v2 != null ? v2 : HttpVersion.DEFAULT );
201     }
202 
203     /* Assert.asserts that two requests are morally equivalent. */
204     public static boolean equivalent(final HttpRequest r1, final HttpRequest r2) {
205         return equivalent(r1.getVersion(), r2.getVersion()) &&
206                 LangUtils.equals(r1.getMethod(), r2.getMethod()) &&
207                 LangUtils.equals(r1.getRequestUri(), r2.getRequestUri()) &&
208                 isEndToEndHeaderSubset(r1, r2);
209     }
210 
211     /* Assert.asserts that two requests are morally equivalent. */
212     public static boolean equivalent(final HttpResponse r1, final HttpResponse r2) {
213         return equivalent(r1.getVersion(), r2.getVersion()) &&
214                 r1.getCode() == r2.getCode() &&
215                 LangUtils.equals(r1.getReasonPhrase(), r2.getReasonPhrase()) &&
216                 isEndToEndHeaderSubset(r1, r2);
217     }
218 
219     public static byte[] getRandomBytes(final int nbytes) {
220         final byte[] bytes = new byte[nbytes];
221         new Random().nextBytes(bytes);
222         return bytes;
223     }
224 
225     public static ByteArrayBuffer getRandomBuffer(final int nbytes) {
226         final ByteArrayBuffer buf = new ByteArrayBuffer(nbytes);
227         buf.setLength(nbytes);
228         new Random().nextBytes(buf.array());
229         return buf;
230     }
231 
232     /** Generates a response body with random content.
233      *  @param nbytes length of the desired response body
234      *  @return an {@link HttpEntity}
235      */
236     public static HttpEntity makeBody(final int nbytes) {
237         return new ByteArrayEntity(getRandomBytes(nbytes), null);
238     }
239 
240     public static HttpCacheEntry makeCacheEntry(final Date requestDate, final Date responseDate) {
241         final Date when = new Date((responseDate.getTime() + requestDate.getTime()) / 2);
242         return makeCacheEntry(requestDate, responseDate, getStockHeaders(when));
243     }
244 
245     public static Header[] getStockHeaders(final Date when) {
246         final Header[] headers = {
247                 new BasicHeader("Date", DateUtils.formatDate(when)),
248                 new BasicHeader("Server", "MockServer/1.0")
249         };
250         return headers;
251     }
252 
253     public static HttpCacheEntry makeCacheEntry(final Date requestDate,
254             final Date responseDate, final Header[] headers) {
255         final byte[] bytes = getRandomBytes(128);
256         return makeCacheEntry(requestDate, responseDate, headers, bytes);
257     }
258 
259     public static HttpCacheEntry makeCacheEntry(final Date requestDate,
260             final Date responseDate, final Header[] headers, final byte[] bytes) {
261         final Map<String,String> variantMap = null;
262         return makeCacheEntry(requestDate, responseDate, headers, bytes,
263                 variantMap);
264     }
265 
266     public static HttpCacheEntry makeCacheEntry(final Map<String,String> variantMap) {
267         final Date now = new Date();
268         return makeCacheEntry(now, now, getStockHeaders(now),
269                 getRandomBytes(128), variantMap);
270     }
271 
272     public static HttpCacheEntry makeCacheEntry(final Date requestDate,
273             final Date responseDate, final Header[] headers, final byte[] bytes,
274             final Map<String,String> variantMap) {
275         return new HttpCacheEntry(requestDate, responseDate, HttpStatus.SC_OK, headers, new HeapResource(bytes), variantMap);
276     }
277 
278     public static HttpCacheEntry makeCacheEntry(final Header[] headers, final byte[] bytes) {
279         final Date now = new Date();
280         return makeCacheEntry(now, now, headers, bytes);
281     }
282 
283     public static HttpCacheEntry makeCacheEntry(final byte[] bytes) {
284         return makeCacheEntry(getStockHeaders(new Date()), bytes);
285     }
286 
287     public static HttpCacheEntry makeCacheEntry(final Header[] headers) {
288         return makeCacheEntry(headers, getRandomBytes(128));
289     }
290 
291     public static HttpCacheEntry makeCacheEntry() {
292         final Date now = new Date();
293         return makeCacheEntry(now, now);
294     }
295 
296     public static HttpCacheEntry makeCacheEntryWithNoRequestMethodOrEntity(final Header[] headers) {
297         final Date now = new Date();
298         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, null, null);
299     }
300 
301     public static HttpCacheEntry makeCacheEntryWithNoRequestMethod(final Header[] headers) {
302         final Date now = new Date();
303         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, new HeapResource(getRandomBytes(128)), null);
304     }
305 
306     public static HttpCacheEntry make204CacheEntryWithNoRequestMethod(final Header[] headers) {
307         final Date now = new Date();
308         return new HttpCacheEntry(now, now, HttpStatus.SC_NO_CONTENT, headers, null, null);
309     }
310 
311     public static HttpCacheEntry makeHeadCacheEntry(final Header[] headers) {
312         final Date now = new Date();
313         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, null, null);
314     }
315 
316     public static HttpCacheEntry makeHeadCacheEntryWithNoRequestMethod(final Header[] headers) {
317         final Date now = new Date();
318         return new HttpCacheEntry(now, now, HttpStatus.SC_OK, headers, null, null);
319     }
320 
321     public static ClassicHttpResponse make200Response() {
322         final ClassicHttpResponse out = new BasicClassicHttpResponse(HttpStatus.SC_OK, "OK");
323         out.setHeader("Date", DateUtils.formatDate(new Date()));
324         out.setHeader("Server", "MockOrigin/1.0");
325         out.setHeader("Content-Length", "128");
326         out.setEntity(makeBody(128));
327         return out;
328     }
329 
330     public static final ClassicHttpResponse make200Response(final Date date, final String cacheControl) {
331         final ClassicHttpResponse response = HttpTestUtils.make200Response();
332         response.setHeader("Date", DateUtils.formatDate(date));
333         response.setHeader("Cache-Control",cacheControl);
334         response.setHeader("Etag","\"etag\"");
335         return response;
336     }
337 
338     public static ClassicHttpResponse make304Response() {
339         return new BasicClassicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not modified");
340     }
341 
342     public static final void assert110WarningFound(final HttpResponse response) {
343         boolean found110Warning = false;
344         final Iterator<HeaderElement> it = MessageSupport.iterate(response, HttpHeaders.WARNING);
345         while (it.hasNext()) {
346             final HeaderElement elt = it.next();
347             final String[] parts = elt.getName().split("\\s");
348             if ("110".equals(parts[0])) {
349                 found110Warning = true;
350                 break;
351             }
352         }
353         Assert.assertTrue(found110Warning);
354     }
355 
356     public static ClassicHttpRequest makeDefaultRequest() {
357         return new BasicClassicHttpRequest(Method.GET.toString(), "/");
358     }
359 
360     public static ClassicHttpRequest makeDefaultHEADRequest() {
361         return new BasicClassicHttpRequest(Method.HEAD.toString(), "/");
362     }
363 
364     public static ClassicHttpResponse make500Response() {
365         return new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
366     }
367 
368     public static Map<String, String> makeDefaultVariantMap(final String key, final String value) {
369         final Map<String, String> variants = new HashMap<>();
370         variants.put(key, value);
371 
372         return variants;
373     }
374 }