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