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.net.URI;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.hc.client5.http.cache.HttpAsyncCacheInvalidator;
35  import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
36  import org.apache.hc.client5.http.cache.HttpCacheEntry;
37  import org.apache.hc.client5.http.impl.Operations;
38  import org.apache.hc.client5.http.utils.URIUtils;
39  import org.apache.hc.core5.annotation.Contract;
40  import org.apache.hc.core5.annotation.Internal;
41  import org.apache.hc.core5.annotation.ThreadingBehavior;
42  import org.apache.hc.core5.concurrent.Cancellable;
43  import org.apache.hc.core5.concurrent.FutureCallback;
44  import org.apache.hc.core5.function.Resolver;
45  import org.apache.hc.core5.http.Header;
46  import org.apache.hc.core5.http.HttpHeaders;
47  import org.apache.hc.core5.http.HttpHost;
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.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  /**
55   * Given a particular HTTP request / response pair, flush any cache entries
56   * that this exchange would invalidate.
57   *
58   * @since 5.0
59   */
60  @Contract(threading = ThreadingBehavior.STATELESS)
61  @Internal
62  public class DefaultAsyncCacheInvalidator extends CacheInvalidatorBase implements HttpAsyncCacheInvalidator {
63  
64      public static final DefaultAsyncCacheInvalidator INSTANCE = new DefaultAsyncCacheInvalidator();
65  
66      private static final Logger LOG = LoggerFactory.getLogger(DefaultAsyncCacheInvalidator.class);
67  
68      private void removeEntry(final HttpAsyncCacheStorage storage, final String cacheKey) {
69          storage.removeEntry(cacheKey, new FutureCallback<Boolean>() {
70  
71              @Override
72              public void completed(final Boolean result) {
73                  if (LOG.isDebugEnabled()) {
74                      if (result.booleanValue()) {
75                          LOG.debug("Cache entry with key {} successfully flushed", cacheKey);
76                      } else {
77                          LOG.debug("Cache entry with key {} could not be flushed", cacheKey);
78                      }
79                  }
80              }
81  
82              @Override
83              public void failed(final Exception ex) {
84                  if (LOG.isWarnEnabled()) {
85                      LOG.warn("Unable to flush cache entry with key {}", cacheKey, ex);
86                  }
87              }
88  
89              @Override
90              public void cancelled() {
91              }
92  
93          });
94      }
95  
96      @Override
97      public Cancellable flushCacheEntriesInvalidatedByRequest(
98              final HttpHost host,
99              final HttpRequest request,
100             final Resolver<URI, String> cacheKeyResolver,
101             final HttpAsyncCacheStorage storage,
102             final FutureCallback<Boolean> callback) {
103         final String s = HttpCacheSupport.getRequestUri(request, host);
104         final URI uri = HttpCacheSupport.normalizeQuietly(s);
105         final String cacheKey = uri != null ? cacheKeyResolver.resolve(uri) : s;
106         return storage.getEntry(cacheKey, new FutureCallback<HttpCacheEntry>() {
107 
108             @Override
109             public void completed(final HttpCacheEntry parentEntry) {
110                 if (requestShouldNotBeCached(request) || shouldInvalidateHeadCacheEntry(request, parentEntry)) {
111                     if (parentEntry != null) {
112                         if (LOG.isDebugEnabled()) {
113                             LOG.debug("Invalidating parentEntry cache entry with key {}", cacheKey);
114                         }
115                         for (final String variantURI : parentEntry.getVariantMap().values()) {
116                             removeEntry(storage, variantURI);
117                         }
118                         removeEntry(storage, cacheKey);
119                     }
120                     if (uri != null) {
121                         if (LOG.isWarnEnabled()) {
122                             LOG.warn("{} is not a valid URI", s);
123                         }
124                         final Header clHdr = request.getFirstHeader(HttpHeaders.CONTENT_LOCATION);
125                         if (clHdr != null) {
126                             final URI contentLocation = HttpCacheSupport.normalizeQuietly(clHdr.getValue());
127                             if (contentLocation != null) {
128                                 if (!flushAbsoluteUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage)) {
129                                     flushRelativeUriFromSameHost(uri, contentLocation, cacheKeyResolver, storage);
130                                 }
131                             }
132                         }
133                         final Header lHdr = request.getFirstHeader(HttpHeaders.LOCATION);
134                         if (lHdr != null) {
135                             final URI location = HttpCacheSupport.normalizeQuietly(lHdr.getValue());
136                             if (location != null) {
137                                 flushAbsoluteUriFromSameHost(uri, location, cacheKeyResolver, storage);
138                             }
139                         }
140                     }
141                 }
142                 callback.completed(Boolean.TRUE);
143             }
144 
145             @Override
146             public void failed(final Exception ex) {
147                 callback.failed(ex);
148             }
149 
150             @Override
151             public void cancelled() {
152                 callback.cancelled();
153             }
154 
155         });
156 
157     }
158 
159     private void flushRelativeUriFromSameHost(
160             final URI requestUri,
161             final URI uri,
162             final Resolver<URI, String> cacheKeyResolver,
163             final HttpAsyncCacheStorage storage) {
164         final URI resolvedUri = uri != null ? URIUtils.resolve(requestUri, uri) : null;
165         if (resolvedUri != null && isSameHost(requestUri, resolvedUri)) {
166             removeEntry(storage, cacheKeyResolver.resolve(resolvedUri));
167         }
168     }
169 
170     private boolean flushAbsoluteUriFromSameHost(
171             final URI requestUri,
172             final URI uri,
173             final Resolver<URI, String> cacheKeyResolver,
174             final HttpAsyncCacheStorage storage) {
175         if (uri != null && isSameHost(requestUri, uri)) {
176             removeEntry(storage, cacheKeyResolver.resolve(uri));
177             return true;
178         }
179         return false;
180     }
181 
182     @Override
183     public Cancellable flushCacheEntriesInvalidatedByExchange(
184             final HttpHost host,
185             final HttpRequest request,
186             final HttpResponse response,
187             final Resolver<URI, String> cacheKeyResolver,
188             final HttpAsyncCacheStorage storage,
189             final FutureCallback<Boolean> callback) {
190         final int status = response.getCode();
191         if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION) {
192             final String s = HttpCacheSupport.getRequestUri(request, host);
193             final URI requestUri = HttpCacheSupport.normalizeQuietly(s);
194             if (requestUri != null) {
195                 final List<String> cacheKeys = new ArrayList<>(2);
196                 final URI contentLocation = getContentLocationURI(requestUri, response);
197                 if (contentLocation != null && isSameHost(requestUri, contentLocation)) {
198                     cacheKeys.add(cacheKeyResolver.resolve(contentLocation));
199                 }
200                 final URI location = getLocationURI(requestUri, response);
201                 if (location != null && isSameHost(requestUri, location)) {
202                     cacheKeys.add(cacheKeyResolver.resolve(location));
203                 }
204                 if (cacheKeys.size() == 1) {
205                     final String key = cacheKeys.get(0);
206                     storage.getEntry(key, new FutureCallback<HttpCacheEntry>() {
207 
208                         @Override
209                         public void completed(final HttpCacheEntry entry) {
210                             if (entry != null) {
211                                 // do not invalidate if response is strictly older than entry
212                                 // or if the etags match
213                                 if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
214                                     removeEntry(storage, key);
215                                 }
216                             }
217                             callback.completed(Boolean.TRUE);
218                         }
219 
220                         @Override
221                         public void failed(final Exception ex) {
222                             callback.failed(ex);
223                         }
224 
225                         @Override
226                         public void cancelled() {
227                             callback.cancelled();
228                         }
229 
230                     });
231                 } else if (cacheKeys.size() > 1) {
232                     storage.getEntries(cacheKeys, new FutureCallback<Map<String, HttpCacheEntry>>() {
233 
234                         @Override
235                         public void completed(final Map<String, HttpCacheEntry> resultMap) {
236                             for (final Map.Entry<String, HttpCacheEntry> resultEntry: resultMap.entrySet()) {
237                                 // do not invalidate if response is strictly older than entry
238                                 // or if the etags match
239                                 final String key = resultEntry.getKey();
240                                 final HttpCacheEntry entry = resultEntry.getValue();
241                                 if (!responseDateOlderThanEntryDate(response, entry) && responseAndEntryEtagsDiffer(response, entry)) {
242                                     removeEntry(storage, key);
243                                 }
244                             }
245                             callback.completed(Boolean.TRUE);
246                         }
247 
248                         @Override
249                         public void failed(final Exception ex) {
250                             callback.failed(ex);
251                         }
252 
253                         @Override
254                         public void cancelled() {
255                             callback.cancelled();
256                         }
257 
258                     });
259                 }
260             }
261         }
262         callback.completed(Boolean.TRUE);
263         return Operations.nonCancellable();
264     }
265 
266 }