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 static org.easymock.EasyMock.anyObject;
30  import static org.easymock.EasyMock.eq;
31  import static org.easymock.EasyMock.expect;
32  import static org.easymock.EasyMock.isA;
33  import static org.easymock.EasyMock.isNull;
34  import static org.easymock.EasyMock.same;
35  import static org.easymock.classextension.EasyMock.createMockBuilder;
36  import static org.easymock.classextension.EasyMock.createNiceMock;
37  import static org.easymock.classextension.EasyMock.replay;
38  import static org.easymock.classextension.EasyMock.verify;
39  import static org.junit.Assert.assertTrue;
40  
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.util.Date;
44  import java.util.HashMap;
45  import java.util.Map;
46  import java.util.concurrent.atomic.AtomicInteger;
47  
48  import org.apache.http.HttpHost;
49  import org.apache.http.HttpRequest;
50  import org.apache.http.HttpResponse;
51  import org.apache.http.HttpStatus;
52  import org.apache.http.HttpVersion;
53  import org.apache.http.client.cache.HttpCacheEntry;
54  import org.apache.http.client.methods.CloseableHttpResponse;
55  import org.apache.http.client.methods.HttpExecutionAware;
56  import org.apache.http.client.methods.HttpGet;
57  import org.apache.http.client.methods.HttpRequestWrapper;
58  import org.apache.http.client.protocol.HttpClientContext;
59  import org.apache.http.client.utils.DateUtils;
60  import org.apache.http.conn.routing.HttpRoute;
61  import org.apache.http.entity.InputStreamEntity;
62  import org.apache.http.impl.execchain.ClientExecChain;
63  import org.apache.http.message.BasicHttpRequest;
64  import org.apache.http.message.BasicHttpResponse;
65  import org.apache.http.message.BasicStatusLine;
66  import org.apache.http.protocol.HTTP;
67  import org.easymock.IExpectationSetters;
68  import org.easymock.EasyMock;
69  import org.junit.Assert;
70  import org.junit.Before;
71  import org.junit.Test;
72  
73  @SuppressWarnings("boxing") // test code
74  public class TestCachingExec extends TestCachingExecChain {
75  
76      private static final String GET_CURRENT_DATE = "getCurrentDate";
77  
78      private static final String HANDLE_BACKEND_RESPONSE = "handleBackendResponse";
79  
80      private static final String CALL_BACKEND = "callBackend";
81  
82      private static final String REVALIDATE_CACHE_ENTRY = "revalidateCacheEntry";
83  
84      private CachingExec impl;
85      private boolean mockedImpl;
86  
87      private CloseableHttpResponse mockBackendResponse;
88  
89      private Date requestDate;
90      private Date responseDate;
91  
92      @Override
93      @Before
94      public void setUp() {
95          super.setUp();
96  
97          mockBackendResponse = createNiceMock(CloseableHttpResponse.class);
98  
99          requestDate = new Date(System.currentTimeMillis() - 1000);
100         responseDate = new Date();
101     }
102 
103     @Override
104     public CachingExec createCachingExecChain(final ClientExecChain mockBackend,
105             final HttpCache mockCache, final CacheValidityPolicy mockValidityPolicy,
106             final ResponseCachingPolicy mockResponsePolicy,
107             final CachedHttpResponseGenerator mockResponseGenerator,
108             final CacheableRequestPolicy mockRequestPolicy,
109             final CachedResponseSuitabilityChecker mockSuitabilityChecker,
110             final ConditionalRequestBuilder mockConditionalRequestBuilder,
111             final ResponseProtocolCompliance mockResponseProtocolCompliance,
112             final RequestProtocolCompliance mockRequestProtocolCompliance,
113             final CacheConfig config, final AsynchronousValidator asyncValidator) {
114         return impl = new CachingExec(
115                 mockBackend,
116                 mockCache,
117                 mockValidityPolicy,
118                 mockResponsePolicy,
119                 mockResponseGenerator,
120                 mockRequestPolicy,
121                 mockSuitabilityChecker,
122                 mockConditionalRequestBuilder,
123                 mockResponseProtocolCompliance,
124                 mockRequestProtocolCompliance,
125                 config,
126                 asyncValidator);
127     }
128 
129     @Override
130     public CachingExec createCachingExecChain(final ClientExecChain backend,
131             final HttpCache cache, final CacheConfig config) {
132         return impl = new CachingExec(backend, cache, config);
133     }
134 
135     @Override
136     protected void replayMocks() {
137         super.replayMocks();
138         replay(mockBackendResponse);
139         if (mockedImpl) {
140             replay(impl);
141         }
142     }
143 
144     @Override
145     protected void verifyMocks() {
146         super.verifyMocks();
147         verify(mockBackendResponse);
148         if (mockedImpl) {
149             verify(impl);
150         }
151     }
152 
153 
154     @Test
155     public void testRequestThatCannotBeServedFromCacheCausesBackendRequest() throws Exception {
156         cacheInvalidatorWasCalled();
157         requestPolicyAllowsCaching(false);
158         mockImplMethods(CALL_BACKEND);
159 
160         implExpectsAnyRequestAndReturn(mockBackendResponse);
161         requestIsFatallyNonCompliant(null);
162 
163         replayMocks();
164         final HttpResponse result = impl.execute(route, request, context);
165         verifyMocks();
166 
167         Assert.assertSame(mockBackendResponse, result);
168     }
169 
170     @Test
171     public void testCacheMissCausesBackendRequest() throws Exception {
172         mockImplMethods(CALL_BACKEND);
173         requestPolicyAllowsCaching(true);
174         getCacheEntryReturns(null);
175         getVariantCacheEntriesReturns(new HashMap<String,Variant>());
176 
177         requestIsFatallyNonCompliant(null);
178 
179         implExpectsAnyRequestAndReturn(mockBackendResponse);
180 
181         replayMocks();
182         final HttpResponse result = impl.execute(route, request, context);
183         verifyMocks();
184 
185         Assert.assertSame(mockBackendResponse, result);
186         Assert.assertEquals(1, impl.getCacheMisses());
187         Assert.assertEquals(0, impl.getCacheHits());
188         Assert.assertEquals(0, impl.getCacheUpdates());
189     }
190 
191     @Test
192     public void testUnsuitableUnvalidatableCacheEntryCausesBackendRequest() throws Exception {
193         mockImplMethods(CALL_BACKEND);
194         requestPolicyAllowsCaching(true);
195         requestIsFatallyNonCompliant(null);
196 
197         getCacheEntryReturns(mockCacheEntry);
198         cacheEntrySuitable(false);
199         cacheEntryValidatable(false);
200         expect(mockConditionalRequestBuilder.buildConditionalRequest(request, mockCacheEntry))
201             .andReturn(request);
202         backendExpectsRequestAndReturn(request, mockBackendResponse);
203         expect(mockBackendResponse.getProtocolVersion()).andReturn(HttpVersion.HTTP_1_1);
204         expect(mockBackendResponse.getStatusLine()).andReturn(
205             new BasicStatusLine(HttpVersion.HTTP_1_1, 200, "Ok"));
206 
207         replayMocks();
208         final HttpResponse result = impl.execute(route, request, context);
209         verifyMocks();
210 
211         Assert.assertSame(mockBackendResponse, result);
212         Assert.assertEquals(0, impl.getCacheMisses());
213         Assert.assertEquals(1, impl.getCacheHits());
214         Assert.assertEquals(1, impl.getCacheUpdates());
215     }
216 
217     @Test
218     public void testUnsuitableValidatableCacheEntryCausesRevalidation() throws Exception {
219         mockImplMethods(REVALIDATE_CACHE_ENTRY);
220         requestPolicyAllowsCaching(true);
221         requestIsFatallyNonCompliant(null);
222 
223         getCacheEntryReturns(mockCacheEntry);
224         cacheEntrySuitable(false);
225         cacheEntryValidatable(true);
226         cacheEntryMustRevalidate(false);
227         cacheEntryProxyRevalidate(false);
228         mayReturnStaleWhileRevalidating(false);
229 
230         expect(impl.revalidateCacheEntry(
231                 isA(HttpRoute.class),
232                 isA(HttpRequestWrapper.class),
233                 isA(HttpClientContext.class),
234                 (HttpExecutionAware) isNull(),
235                 isA(HttpCacheEntry.class))).andReturn(mockBackendResponse);
236 
237         replayMocks();
238         final HttpResponse result = impl.execute(route, request, context);
239         verifyMocks();
240 
241         Assert.assertSame(mockBackendResponse, result);
242         Assert.assertEquals(0, impl.getCacheMisses());
243         Assert.assertEquals(1, impl.getCacheHits());
244         Assert.assertEquals(0, impl.getCacheUpdates());
245     }
246 
247     @Test
248     public void testRevalidationCallsHandleBackEndResponseWhenNot200Or304() throws Exception {
249         mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
250 
251         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
252                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
253         final CloseableHttpResponse originResponse = Proxies.enhanceResponse(
254                 new BasicHttpResponse(HttpVersion.HTTP_1_1,  HttpStatus.SC_NOT_FOUND, "Not Found"));
255         final CloseableHttpResponse finalResponse = Proxies.enhanceResponse(
256                 HttpTestUtils.make200Response());
257 
258         conditionalRequestBuilderReturns(validate);
259         getCurrentDateReturns(requestDate);
260         backendExpectsRequestAndReturn(validate, originResponse);
261         getCurrentDateReturns(responseDate);
262         expect(impl.handleBackendResponse(
263                 same(validate),
264                 same(context),
265                 eq(requestDate),
266                 eq(responseDate),
267                 same(originResponse))).andReturn(finalResponse);
268 
269         replayMocks();
270         final HttpResponse result =
271             impl.revalidateCacheEntry(route, request, context, null, entry);
272         verifyMocks();
273 
274         Assert.assertSame(finalResponse, result);
275     }
276 
277     @Test
278     public void testRevalidationUpdatesCacheEntryAndPutsItToCacheWhen304ReturningCachedResponse()
279             throws Exception {
280 
281         mockImplMethods(GET_CURRENT_DATE);
282 
283         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
284                 new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1));
285         final HttpResponse originResponse = Proxies.enhanceResponse(
286             new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified"));
287         final HttpCacheEntry updatedEntry = HttpTestUtils.makeCacheEntry();
288 
289         conditionalRequestBuilderReturns(validate);
290         getCurrentDateReturns(requestDate);
291         backendExpectsRequestAndReturn(validate, originResponse);
292         getCurrentDateReturns(responseDate);
293         expect(mockCache.updateCacheEntry(
294                 eq(host),
295                 same(request),
296                 same(entry),
297                 same(originResponse),
298                 eq(requestDate),
299                 eq(responseDate)))
300             .andReturn(updatedEntry);
301         expect(mockSuitabilityChecker.isConditional(request)).andReturn(false);
302         responseIsGeneratedFromCache();
303 
304         replayMocks();
305         impl.revalidateCacheEntry(route, request, context, null, entry);
306         verifyMocks();
307     }
308 
309     @Test
310     public void testRevalidationRewritesAbsoluteUri() throws Exception {
311 
312         mockImplMethods(GET_CURRENT_DATE);
313 
314         // Fail on an unexpected request, rather than causing a later NPE
315         EasyMock.resetToStrict(mockBackend);
316 
317         final HttpRequestWrapper validate = HttpRequestWrapper.wrap(
318                 new HttpGet("http://foo.example.com/resource"));
319         final HttpRequestWrapper relativeValidate = HttpRequestWrapper.wrap(
320                 new BasicHttpRequest("GET", "/resource", HttpVersion.HTTP_1_1));
321         final CloseableHttpResponse originResponse = Proxies.enhanceResponse(
322             new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "Okay"));
323 
324         conditionalRequestBuilderReturns(validate);
325         getCurrentDateReturns(requestDate);
326 
327         final CloseableHttpResponse resp = mockBackend.execute(EasyMock.isA(HttpRoute.class),
328                 eqRequest(relativeValidate), EasyMock.isA(HttpClientContext.class),
329                 EasyMock.<HttpExecutionAware> isNull());
330         expect(resp).andReturn(originResponse);
331 
332         getCurrentDateReturns(responseDate);
333 
334         replayMocks();
335         impl.revalidateCacheEntry(route, request, context, null, entry);
336         verifyMocks();
337     }
338 
339     @Test
340     public void testEndlessResponsesArePassedThrough() throws Exception {
341         impl = createCachingExecChain(mockBackend, new BasicHttpCache(), CacheConfig.DEFAULT);
342 
343         final HttpResponse resp1 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
344         resp1.setHeader("Date", DateUtils.formatDate(new Date()));
345         resp1.setHeader("Server", "MockOrigin/1.0");
346         resp1.setHeader(HTTP.TRANSFER_ENCODING, HTTP.CHUNK_CODING);
347 
348         final AtomicInteger size = new AtomicInteger();
349         final AtomicInteger maxlength = new AtomicInteger(Integer.MAX_VALUE);
350         resp1.setEntity(new InputStreamEntity(new InputStream() {
351             private Throwable closed;
352 
353             @Override
354             public void close() throws IOException {
355                 closed = new Throwable();
356                 super.close();
357             }
358 
359             @Override
360             public int read() throws IOException {
361                 Thread.yield();
362                 if (closed != null) {
363                     throw new IOException("Response has been closed");
364 
365                 }
366                 if (size.incrementAndGet() > maxlength.get()) {
367                     return -1;
368                 }
369                 return 'y';
370             }
371         }, -1));
372 
373         final CloseableHttpResponse resp = mockBackend.execute(
374                 EasyMock.isA(HttpRoute.class),
375                 EasyMock.isA(HttpRequestWrapper.class),
376                 EasyMock.isA(HttpClientContext.class),
377                 EasyMock.<HttpExecutionAware>isNull());
378         EasyMock.expect(resp).andReturn(Proxies.enhanceResponse(resp1));
379 
380         final HttpRequestWrapper req1 = HttpRequestWrapper.wrap(HttpTestUtils.makeDefaultRequest());
381 
382         replayMocks();
383         final CloseableHttpResponse resp2 = impl.execute(route, req1, context, null);
384         maxlength.set(size.get() * 2);
385         verifyMocks();
386         assertTrue(HttpTestUtils.semanticallyTransparent(resp1, resp2));
387     }
388 
389     @Test
390     public void testCallBackendMakesBackEndRequestAndHandlesResponse() throws Exception {
391         mockImplMethods(GET_CURRENT_DATE, HANDLE_BACKEND_RESPONSE);
392         final HttpResponse resp = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
393         getCurrentDateReturns(requestDate);
394         backendExpectsRequestAndReturn(request, resp);
395         getCurrentDateReturns(responseDate);
396         handleBackendResponseReturnsResponse(request, resp);
397 
398         replayMocks();
399 
400         impl.callBackend(route, request, context, null);
401 
402         verifyMocks();
403     }
404 
405     @Test
406     public void testDoesNotFlushCachesOnCacheHit() throws Exception {
407         requestPolicyAllowsCaching(true);
408         requestIsFatallyNonCompliant(null);
409 
410         getCacheEntryReturns(mockCacheEntry);
411         doesNotFlushCache();
412         cacheEntrySuitable(true);
413         cacheEntryValidatable(true);
414 
415         expect(mockResponseGenerator.generateResponse(isA(HttpRequestWrapper.class), isA(HttpCacheEntry.class)))
416                 .andReturn(mockBackendResponse);
417 
418         replayMocks();
419         final HttpResponse result = impl.execute(route, request, context);
420         verifyMocks();
421 
422         Assert.assertSame(mockBackendResponse, result);
423     }
424 
425     private IExpectationSetters<CloseableHttpResponse> implExpectsAnyRequestAndReturn(
426             final CloseableHttpResponse response) throws Exception {
427         final CloseableHttpResponse resp = impl.callBackend(
428                 EasyMock.isA(HttpRoute.class),
429                 EasyMock.isA(HttpRequestWrapper.class),
430                 EasyMock.isA(HttpClientContext.class),
431                 EasyMock.<HttpExecutionAware>isNull());
432         return EasyMock.expect(resp).andReturn(response);
433     }
434 
435     private void getVariantCacheEntriesReturns(final Map<String,Variant> result) throws IOException {
436         expect(mockCache.getVariantCacheEntriesWithEtags(host, request)).andReturn(result);
437     }
438 
439     private void cacheInvalidatorWasCalled()  throws IOException {
440         mockCache.flushInvalidatedCacheEntriesFor(
441                 (HttpHost)anyObject(),
442                 (HttpRequest)anyObject());
443     }
444 
445     private void getCurrentDateReturns(final Date date) {
446         expect(impl.getCurrentDate()).andReturn(date);
447     }
448 
449     private void handleBackendResponseReturnsResponse(final HttpRequestWrapper request, final HttpResponse response)
450             throws IOException {
451         expect(
452                 impl.handleBackendResponse(
453                         same(request),
454                         isA(HttpClientContext.class),
455                         isA(Date.class),
456                         isA(Date.class),
457                         isA(CloseableHttpResponse.class))).andReturn(
458                                 Proxies.enhanceResponse(response));
459     }
460 
461     private void mockImplMethods(final String... methods) {
462         mockedImpl = true;
463         impl = createMockBuilder(CachingExec.class).withConstructor(
464                 mockBackend,
465                 mockCache,
466                 mockValidityPolicy,
467                 mockResponsePolicy,
468                 mockResponseGenerator,
469                 mockRequestPolicy,
470                 mockSuitabilityChecker,
471                 mockConditionalRequestBuilder,
472                 mockResponseProtocolCompliance,
473                 mockRequestProtocolCompliance,
474                 config,
475                 asyncValidator).addMockedMethods(methods).createNiceMock();
476     }
477 
478 }