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