| /* |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals on behalf of the Apache Software Foundation. For more |
| * information on the Apache Software Foundation, please see |
| * <http://www.apache.org/>. |
| * |
| */ |
| package org.apache.hc.client5.http.impl.cache; |
| |
| import java.io.IOException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.Date; |
| |
| import org.apache.hc.client5.http.cache.HeaderConstants; |
| import org.apache.hc.client5.http.cache.HttpCacheEntry; |
| import org.apache.hc.client5.http.cache.HttpCacheInvalidator; |
| import org.apache.hc.client5.http.cache.HttpCacheStorage; |
| import org.apache.hc.client5.http.utils.DateUtils; |
| import org.apache.hc.core5.annotation.Contract; |
| import org.apache.hc.core5.annotation.ThreadingBehavior; |
| import org.apache.hc.core5.http.Header; |
| import org.apache.hc.core5.http.HttpHeaders; |
| import org.apache.hc.core5.http.HttpHost; |
| import org.apache.hc.core5.http.HttpRequest; |
| import org.apache.hc.core5.http.HttpResponse; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| |
| /** |
| * Given a particular HttpRequest, flush any cache entries that this request |
| * would invalidate. |
| * |
| * @since 4.1 |
| */ |
| @Contract(threading = ThreadingBehavior.IMMUTABLE) |
| class CacheInvalidator implements HttpCacheInvalidator { |
| |
| private final HttpCacheStorage storage; |
| private final CacheKeyGenerator cacheKeyGenerator; |
| |
| private final Logger log = LogManager.getLogger(getClass()); |
| |
| /** |
| * Create a new {@link CacheInvalidator} for a given {@link HttpCache} and |
| * {@link CacheKeyGenerator}. |
| * |
| * @param uriExtractor Provides identifiers for the keys to store cache entries |
| * @param storage the cache to store items away in |
| */ |
| public CacheInvalidator( |
| final CacheKeyGenerator uriExtractor, |
| final HttpCacheStorage storage) { |
| this.cacheKeyGenerator = uriExtractor; |
| this.storage = storage; |
| } |
| |
| /** |
| * Remove cache entries from the cache that are no longer fresh or |
| * have been invalidated in some way. |
| * |
| * @param host The backend host we are talking to |
| * @param req The HttpRequest to that host |
| */ |
| @Override |
| public void flushInvalidatedCacheEntries(final HttpHost host, final HttpRequest req) { |
| final String theUri = cacheKeyGenerator.generateKey(host, req); |
| final HttpCacheEntry parent = getEntry(theUri); |
| |
| if (requestShouldNotBeCached(req) || shouldInvalidateHeadCacheEntry(req, parent)) { |
| log.debug("Invalidating parent cache entry: " + parent); |
| if (parent != null) { |
| for (final String variantURI : parent.getVariantMap().values()) { |
| flushEntry(variantURI); |
| } |
| flushEntry(theUri); |
| } |
| final URL reqURL = getAbsoluteURL(theUri); |
| if (reqURL == null) { |
| log.error("Couldn't transform request into valid URL"); |
| return; |
| } |
| final Header clHdr = req.getFirstHeader("Content-Location"); |
| if (clHdr != null) { |
| final String contentLocation = clHdr.getValue(); |
| if (!flushAbsoluteUriFromSameHost(reqURL, contentLocation)) { |
| flushRelativeUriFromSameHost(reqURL, contentLocation); |
| } |
| } |
| final Header lHdr = req.getFirstHeader("Location"); |
| if (lHdr != null) { |
| flushAbsoluteUriFromSameHost(reqURL, lHdr.getValue()); |
| } |
| } |
| } |
| |
| private boolean shouldInvalidateHeadCacheEntry(final HttpRequest req, final HttpCacheEntry parentCacheEntry) { |
| return requestIsGet(req) && isAHeadCacheEntry(parentCacheEntry); |
| } |
| |
| private boolean requestIsGet(final HttpRequest req) { |
| return req.getMethod().equals((HeaderConstants.GET_METHOD)); |
| } |
| |
| private boolean isAHeadCacheEntry(final HttpCacheEntry parentCacheEntry) { |
| return parentCacheEntry != null && parentCacheEntry.getRequestMethod().equals(HeaderConstants.HEAD_METHOD); |
| } |
| |
| private void flushEntry(final String uri) { |
| try { |
| storage.removeEntry(uri); |
| } catch (final IOException ioe) { |
| log.warn("unable to flush cache entry", ioe); |
| } |
| } |
| |
| private HttpCacheEntry getEntry(final String theUri) { |
| try { |
| return storage.getEntry(theUri); |
| } catch (final IOException ioe) { |
| log.warn("could not retrieve entry from storage", ioe); |
| } |
| return null; |
| } |
| |
| protected void flushUriIfSameHost(final URL requestURL, final URL targetURL) { |
| final URL canonicalTarget = getAbsoluteURL(cacheKeyGenerator.generateKey(targetURL)); |
| if (canonicalTarget == null) { |
| return; |
| } |
| if (canonicalTarget.getAuthority().equalsIgnoreCase(requestURL.getAuthority())) { |
| flushEntry(canonicalTarget.toString()); |
| } |
| } |
| |
| protected void flushRelativeUriFromSameHost(final URL reqURL, final String relUri) { |
| final URL relURL = getRelativeURL(reqURL, relUri); |
| if (relURL == null) { |
| return; |
| } |
| flushUriIfSameHost(reqURL, relURL); |
| } |
| |
| |
| protected boolean flushAbsoluteUriFromSameHost(final URL reqURL, final String uri) { |
| final URL absURL = getAbsoluteURL(uri); |
| if (absURL == null) { |
| return false; |
| } |
| flushUriIfSameHost(reqURL,absURL); |
| return true; |
| } |
| |
| private URL getAbsoluteURL(final String uri) { |
| URL absURL = null; |
| try { |
| absURL = new URL(uri); |
| } catch (final MalformedURLException mue) { |
| // nop |
| } |
| return absURL; |
| } |
| |
| private URL getRelativeURL(final URL reqURL, final String relUri) { |
| URL relURL = null; |
| try { |
| relURL = new URL(reqURL,relUri); |
| } catch (final MalformedURLException e) { |
| // nop |
| } |
| return relURL; |
| } |
| |
| protected boolean requestShouldNotBeCached(final HttpRequest req) { |
| final String method = req.getMethod(); |
| return notGetOrHeadRequest(method); |
| } |
| |
| private boolean notGetOrHeadRequest(final String method) { |
| return !(HeaderConstants.GET_METHOD.equals(method) || HeaderConstants.HEAD_METHOD |
| .equals(method)); |
| } |
| |
| /** Flushes entries that were invalidated by the given response |
| * received for the given host/request pair. |
| */ |
| @Override |
| public void flushInvalidatedCacheEntries(final HttpHost host, |
| final HttpRequest request, final HttpResponse response) { |
| final int status = response.getCode(); |
| if (status < 200 || status > 299) { |
| return; |
| } |
| final URL reqURL = getAbsoluteURL(cacheKeyGenerator.generateKey(host, request)); |
| if (reqURL == null) { |
| return; |
| } |
| final URL contentLocation = getContentLocationURL(reqURL, response); |
| if (contentLocation != null) { |
| flushLocationCacheEntry(reqURL, response, contentLocation); |
| } |
| final URL location = getLocationURL(reqURL, response); |
| if (location != null) { |
| flushLocationCacheEntry(reqURL, response, location); |
| } |
| } |
| |
| private void flushLocationCacheEntry(final URL reqURL, |
| final HttpResponse response, final URL location) { |
| final String cacheKey = cacheKeyGenerator.generateKey(location); |
| final HttpCacheEntry entry = getEntry(cacheKey); |
| if (entry == null) { |
| return; |
| } |
| |
| // do not invalidate if response is strictly older than entry |
| // or if the etags match |
| |
| if (responseDateOlderThanEntryDate(response, entry)) { |
| return; |
| } |
| if (!responseAndEntryEtagsDiffer(response, entry)) { |
| return; |
| } |
| |
| flushUriIfSameHost(reqURL, location); |
| } |
| |
| private URL getContentLocationURL(final URL reqURL, final HttpResponse response) { |
| final Header clHeader = response.getFirstHeader("Content-Location"); |
| if (clHeader == null) { |
| return null; |
| } |
| final String contentLocation = clHeader.getValue(); |
| final URL canonURL = getAbsoluteURL(contentLocation); |
| if (canonURL != null) { |
| return canonURL; |
| } |
| return getRelativeURL(reqURL, contentLocation); |
| } |
| |
| private URL getLocationURL(final URL reqURL, final HttpResponse response) { |
| final Header clHeader = response.getFirstHeader("Location"); |
| if (clHeader == null) { |
| return null; |
| } |
| final String location = clHeader.getValue(); |
| final URL canonURL = getAbsoluteURL(location); |
| if (canonURL != null) { |
| return canonURL; |
| } |
| return getRelativeURL(reqURL, location); |
| } |
| |
| private boolean responseAndEntryEtagsDiffer(final HttpResponse response, |
| final HttpCacheEntry entry) { |
| final Header entryEtag = entry.getFirstHeader(HeaderConstants.ETAG); |
| final Header responseEtag = response.getFirstHeader(HeaderConstants.ETAG); |
| if (entryEtag == null || responseEtag == null) { |
| return false; |
| } |
| return (!entryEtag.getValue().equals(responseEtag.getValue())); |
| } |
| |
| private boolean responseDateOlderThanEntryDate(final HttpResponse response, |
| final HttpCacheEntry entry) { |
| final Header entryDateHeader = entry.getFirstHeader(HttpHeaders.DATE); |
| final Header responseDateHeader = response.getFirstHeader(HttpHeaders.DATE); |
| if (entryDateHeader == null || responseDateHeader == null) { |
| /* be conservative; should probably flush */ |
| return false; |
| } |
| final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue()); |
| final Date responseDate = DateUtils.parseDate(responseDateHeader.getValue()); |
| if (entryDate == null || responseDate == null) { |
| return false; |
| } |
| return responseDate.before(entryDate); |
| } |
| } |