| /* |
| * ==================================================================== |
| * 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.util.Date; |
| import java.util.Iterator; |
| |
| import org.apache.hc.client5.http.cache.HeaderConstants; |
| import org.apache.hc.client5.http.cache.HttpCacheEntry; |
| 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.HeaderElement; |
| import org.apache.hc.core5.http.HttpHost; |
| import org.apache.hc.core5.http.HttpRequest; |
| import org.apache.hc.core5.http.HttpStatus; |
| import org.apache.hc.core5.http.message.MessageSupport; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| |
| /** |
| * Determines whether a given {@link HttpCacheEntry} is suitable to be |
| * used as a response for a given {@link HttpRequest}. |
| * |
| * @since 4.1 |
| */ |
| @Contract(threading = ThreadingBehavior.IMMUTABLE) |
| class CachedResponseSuitabilityChecker { |
| |
| private final Logger log = LogManager.getLogger(getClass()); |
| |
| private final boolean sharedCache; |
| private final boolean useHeuristicCaching; |
| private final float heuristicCoefficient; |
| private final long heuristicDefaultLifetime; |
| private final CacheValidityPolicy validityStrategy; |
| |
| CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy, |
| final CacheConfig config) { |
| super(); |
| this.validityStrategy = validityStrategy; |
| this.sharedCache = config.isSharedCache(); |
| this.useHeuristicCaching = config.isHeuristicCachingEnabled(); |
| this.heuristicCoefficient = config.getHeuristicCoefficient(); |
| this.heuristicDefaultLifetime = config.getHeuristicDefaultLifetime(); |
| } |
| |
| CachedResponseSuitabilityChecker(final CacheConfig config) { |
| this(new CacheValidityPolicy(), config); |
| } |
| |
| private boolean isFreshEnough(final HttpCacheEntry entry, final HttpRequest request, final Date now) { |
| if (validityStrategy.isResponseFresh(entry, now)) { |
| return true; |
| } |
| if (useHeuristicCaching && |
| validityStrategy.isResponseHeuristicallyFresh(entry, now, heuristicCoefficient, heuristicDefaultLifetime)) { |
| return true; |
| } |
| if (originInsistsOnFreshness(entry)) { |
| return false; |
| } |
| final long maxstale = getMaxStale(request); |
| if (maxstale == -1) { |
| return false; |
| } |
| return (maxstale > validityStrategy.getStalenessSecs(entry, now)); |
| } |
| |
| private boolean originInsistsOnFreshness(final HttpCacheEntry entry) { |
| if (validityStrategy.mustRevalidate(entry)) { |
| return true; |
| } |
| if (!sharedCache) { |
| return false; |
| } |
| return validityStrategy.proxyRevalidate(entry) || |
| validityStrategy.hasCacheControlDirective(entry, "s-maxage"); |
| } |
| |
| private long getMaxStale(final HttpRequest request) { |
| long maxstale = -1; |
| final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL); |
| while (it.hasNext()) { |
| final HeaderElement elt = it.next(); |
| if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) { |
| if ((elt.getValue() == null || "".equals(elt.getValue().trim())) && maxstale == -1) { |
| maxstale = Long.MAX_VALUE; |
| } else { |
| try { |
| long val = Long.parseLong(elt.getValue()); |
| if (val < 0) { |
| val = 0; |
| } |
| if (maxstale == -1 || val < maxstale) { |
| maxstale = val; |
| } |
| } catch (final NumberFormatException nfe) { |
| // err on the side of preserving semantic transparency |
| maxstale = 0; |
| } |
| } |
| } |
| } |
| return maxstale; |
| } |
| |
| /** |
| * Determine if I can utilize a {@link HttpCacheEntry} to respond to the given |
| * {@link HttpRequest} |
| * |
| * @param host |
| * {@link HttpHost} |
| * @param request |
| * {@link HttpRequest} |
| * @param entry |
| * {@link HttpCacheEntry} |
| * @param now |
| * Right now in time |
| * @return boolean yes/no answer |
| */ |
| public boolean canCachedResponseBeUsed(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry, final Date now) { |
| if (!isFreshEnough(entry, request, now)) { |
| log.trace("Cache entry was not fresh enough"); |
| return false; |
| } |
| |
| if (isGet(request) && !validityStrategy.contentLengthHeaderMatchesActualLength(entry)) { |
| log.debug("Cache entry Content-Length and header information do not match"); |
| return false; |
| } |
| |
| if (hasUnsupportedConditionalHeaders(request)) { |
| log.debug("Request contained conditional headers we don't handle"); |
| return false; |
| } |
| |
| if (!isConditional(request) && entry.getStatus() == HttpStatus.SC_NOT_MODIFIED) { |
| return false; |
| } |
| |
| if (isConditional(request) && !allConditionalsMatch(request, entry, now)) { |
| return false; |
| } |
| |
| if (hasUnsupportedCacheEntryForGet(request, entry)) { |
| log.debug("HEAD response caching enabled but the cache entry does not contain a " + |
| "request method, entity or a 204 response"); |
| return false; |
| } |
| final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL); |
| while (it.hasNext()) { |
| final HeaderElement elt = it.next(); |
| if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) { |
| log.trace("Response contained NO CACHE directive, cache was not suitable"); |
| return false; |
| } |
| |
| if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elt.getName())) { |
| log.trace("Response contained NO STORE directive, cache was not suitable"); |
| return false; |
| } |
| |
| if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) { |
| try { |
| final int maxage = Integer.parseInt(elt.getValue()); |
| if (validityStrategy.getCurrentAgeSecs(entry, now) > maxage) { |
| log.trace("Response from cache was NOT suitable due to max age"); |
| return false; |
| } |
| } catch (final NumberFormatException ex) { |
| // err conservatively |
| log.debug("Response from cache was malformed" + ex.getMessage()); |
| return false; |
| } |
| } |
| |
| if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) { |
| try { |
| final int maxstale = Integer.parseInt(elt.getValue()); |
| if (validityStrategy.getFreshnessLifetimeSecs(entry) > maxstale) { |
| log.trace("Response from cache was not suitable due to Max stale freshness"); |
| return false; |
| } |
| } catch (final NumberFormatException ex) { |
| // err conservatively |
| log.debug("Response from cache was malformed: " + ex.getMessage()); |
| return false; |
| } |
| } |
| |
| if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())) { |
| try { |
| final long minfresh = Long.parseLong(elt.getValue()); |
| if (minfresh < 0L) { |
| return false; |
| } |
| final long age = validityStrategy.getCurrentAgeSecs(entry, now); |
| final long freshness = validityStrategy.getFreshnessLifetimeSecs(entry); |
| if (freshness - age < minfresh) { |
| log.trace("Response from cache was not suitable due to min fresh " + |
| "freshness requirement"); |
| return false; |
| } |
| } catch (final NumberFormatException ex) { |
| // err conservatively |
| log.debug("Response from cache was malformed: " + ex.getMessage()); |
| return false; |
| } |
| } |
| } |
| |
| log.trace("Response from cache was suitable"); |
| return true; |
| } |
| |
| private boolean isGet(final HttpRequest request) { |
| return request.getMethod().equals(HeaderConstants.GET_METHOD); |
| } |
| |
| private boolean entryIsNotA204Response(final HttpCacheEntry entry) { |
| return entry.getStatus() != HttpStatus.SC_NO_CONTENT; |
| } |
| |
| private boolean cacheEntryDoesNotContainMethodAndEntity(final HttpCacheEntry entry) { |
| return entry.getRequestMethod() == null && entry.getResource() == null; |
| } |
| |
| private boolean hasUnsupportedCacheEntryForGet(final HttpRequest request, final HttpCacheEntry entry) { |
| return isGet(request) && cacheEntryDoesNotContainMethodAndEntity(entry) && entryIsNotA204Response(entry); |
| } |
| |
| /** |
| * Is this request the type of conditional request we support? |
| * @param request The current httpRequest being made |
| * @return {@code true} if the request is supported |
| */ |
| public boolean isConditional(final HttpRequest request) { |
| return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request); |
| } |
| |
| /** |
| * Check that conditionals that are part of this request match |
| * @param request The current httpRequest being made |
| * @param entry the cache entry |
| * @param now right NOW in time |
| * @return {@code true} if the request matches all conditionals |
| */ |
| public boolean allConditionalsMatch(final HttpRequest request, final HttpCacheEntry entry, final Date now) { |
| final boolean hasEtagValidator = hasSupportedEtagValidator(request); |
| final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request); |
| |
| final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry); |
| final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now); |
| |
| if ((hasEtagValidator && hasLastModifiedValidator) |
| && !(etagValidatorMatches && lastModifiedValidatorMatches)) { |
| return false; |
| } else if (hasEtagValidator && !etagValidatorMatches) { |
| return false; |
| } |
| |
| if (hasLastModifiedValidator && !lastModifiedValidatorMatches) { |
| return false; |
| } |
| return true; |
| } |
| |
| private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) { |
| return (request.getFirstHeader(HeaderConstants.IF_RANGE) != null |
| || request.getFirstHeader(HeaderConstants.IF_MATCH) != null |
| || hasValidDateField(request, HeaderConstants.IF_UNMODIFIED_SINCE)); |
| } |
| |
| private boolean hasSupportedEtagValidator(final HttpRequest request) { |
| return request.containsHeader(HeaderConstants.IF_NONE_MATCH); |
| } |
| |
| private boolean hasSupportedLastModifiedValidator(final HttpRequest request) { |
| return hasValidDateField(request, HeaderConstants.IF_MODIFIED_SINCE); |
| } |
| |
| /** |
| * Check entry against If-None-Match |
| * @param request The current httpRequest being made |
| * @param entry the cache entry |
| * @return boolean does the etag validator match |
| */ |
| private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) { |
| final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG); |
| final String etag = (etagHeader != null) ? etagHeader.getValue() : null; |
| final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.IF_NONE_MATCH); |
| while (it.hasNext()) { |
| final HeaderElement elt = it.next(); |
| final String reqEtag = elt.toString(); |
| if (("*".equals(reqEtag) && etag != null) || reqEtag.equals(etag)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Check entry against If-Modified-Since, if If-Modified-Since is in the future it is invalid as per |
| * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html |
| * @param request The current httpRequest being made |
| * @param entry the cache entry |
| * @param now right NOW in time |
| * @return boolean Does the last modified header match |
| */ |
| private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Date now) { |
| final Header lastModifiedHeader = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED); |
| Date lastModified = null; |
| if (lastModifiedHeader != null) { |
| lastModified = DateUtils.parseDate(lastModifiedHeader.getValue()); |
| } |
| if (lastModified == null) { |
| return false; |
| } |
| |
| for (final Header h : request.getHeaders(HeaderConstants.IF_MODIFIED_SINCE)) { |
| final Date ifModifiedSince = DateUtils.parseDate(h.getValue()); |
| if (ifModifiedSince != null) { |
| if (ifModifiedSince.after(now) || lastModified.after(ifModifiedSince)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| private boolean hasValidDateField(final HttpRequest request, final String headerName) { |
| for(final Header h : request.getHeaders(headerName)) { |
| final Date date = DateUtils.parseDate(h.getValue()); |
| return date != null; |
| } |
| return false; |
| } |
| } |