CacheHeader.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 wcm.io
* %%
* Licensed 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.
* #L%
*/
package io.wcm.wcm.commons.caching;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.annotation.versioning.ProviderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.wcm.api.WCMMode;
/**
* Contains common functionality to control client-side caching.
*/
@ProviderType
public final class CacheHeader {
private CacheHeader() {
// utility methods only
}
private static final Logger log = LoggerFactory.getLogger(CacheHeader.class);
private static final String RFC_1123_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z";
static final String HEADER_LAST_MODIFIED = "Last-Modified";
static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
static final String HEADER_PRAGMA = "Pragma";
static final String HEADER_CACHE_CONTROL = "Cache-Control";
static final String HEADER_EXPIRES = "Expires";
static final String HEADER_DISPATCHER = "Dispatcher";
static final String NO_CACHE = "no-cache";
/**
* shared instance of the RFC1123 date format, must not be used directly but only using the synchronized {@link #formatDate(Date)} and
* {@link #parseDate(String)} methods
*/
@SuppressWarnings("java:S2885")
private static final DateFormat RFC1123_DATE_FORMAT = new SimpleDateFormat(RFC_1123_DATE_PATTERN, Locale.US);
static {
// all times are written and parsed in GMT
RFC1123_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
}
static synchronized String formatDate(Date pDate) {
return RFC1123_DATE_FORMAT.format(pDate);
}
static synchronized Date parseDate(String pDateString) throws ParseException {
return RFC1123_DATE_FORMAT.parse(pDateString);
}
/**
* Compares the "If-Modified-Since header" of the incoming request with the last modification date of a resource. If
* the resource was not modified since the client retrieved the resource, a 304-redirect is send to the response (and
* the method returns true). If the resource has changed (or the client didn't) supply the "If-Modified-Since" header
* a "Last-Modified" header is set so future requests can be cached.
* <p>
* Expires header is automatically set on author instance, and not set on publish instance.
* </p>
* @param resource the JCR resource the last modification date is taken from
* @param request Request
* @param response Response
* @return true if the method send a 304 redirect, so that the caller shouldn't write any output to the response
* stream
*/
public static boolean isNotModified(@NotNull Resource resource,
@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) {
ResourceModificationDateProvider dateProvider = new ResourceModificationDateProvider(resource);
return isNotModified(dateProvider, request, response);
}
/**
* Compares the "If-Modified-Since header" of the incoming request with the last modification date of a resource. If
* the resource was not modified since the client retrieved the resource, a 304-redirect is send to the response (and
* the method returns true). If the resource has changed (or the client didn't) supply the "If-Modified-Since" header
* a "Last-Modified" header is set so future requests can be cached.
* @param resource the JCR resource the last modification date is taken from
* @param request Request
* @param response Response
* @param setExpiresHeader Set expires header to -1 to ensure the browser checks for a new version on every request.
* @return true if the method send a 304 redirect, so that the caller shouldn't write any output to the response
* stream
*/
public static boolean isNotModified(@NotNull Resource resource,
@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response, boolean setExpiresHeader) {
ResourceModificationDateProvider dateProvider = new ResourceModificationDateProvider(resource);
return isNotModified(dateProvider, request, response, setExpiresHeader);
}
/**
* Compares the "If-Modified-Since header" of the incoming request with the last modification date of an aggregated
* resource. If the resource was not modified since the client retrieved the resource, a 304-redirect is send to the
* response (and the method returns true). If the resource has changed (or the client didn't) supply the
* "If-Modified-Since" header a "Last-Modified" header is set so future requests can be cached.
* <p>
* Expires header is automatically set on author instance, and not set on publish instance.
* </p>
* @param dateProvider abstraction layer that calculates the last-modification time of an aggregated resource
* @param request Request
* @param response Response
* @return true if the method send a 304 redirect, so that the caller shouldn't write any output to the response
* stream
*/
public static boolean isNotModified(@NotNull ModificationDateProvider dateProvider,
@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) {
boolean isAuthor = WCMMode.fromRequest(request) != WCMMode.DISABLED;
return isNotModified(dateProvider, request, response, isAuthor);
}
/**
* Compares the "If-Modified-Since header" of the incoming request with the last modification date of an aggregated
* resource. If the resource was not modified since the client retrieved the resource, a 304-redirect is send to the
* response (and the method returns true). If the resource has changed (or the client didn't) supply the
* "If-Modified-Since" header a "Last-Modified" header is set so future requests can be cached.
* @param dateProvider abstraction layer that calculates the last-modification time of an aggregated resource
* @param request Request
* @param response Response
* @param setExpiresHeader Set expires header to -1 to ensure the browser checks for a new version on every request.
* @return true if the method send a 304 redirect, so that the caller shouldn't write any output to the response
* stream
*/
public static boolean isNotModified(@NotNull ModificationDateProvider dateProvider,
@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response, boolean setExpiresHeader) {
// assume the resource *was* modified until we know better
boolean isModified = true;
// get the modification date of the resource(s) in question
Date lastModificationDate = dateProvider.getModificationDate();
// get the date of the version from the client's cache
String ifModifiedSince = request.getHeader(HEADER_IF_MODIFIED_SINCE);
// only compare if both resource modification date and If-Modified-Since header is available
if (lastModificationDate != null && StringUtils.isNotBlank(ifModifiedSince)) {
try {
Date clientModificationDate = parseDate(ifModifiedSince);
// resource is considered modified if it's modification date is *after* the client's modification date
isModified = lastModificationDate.getTime() - DateUtils.MILLIS_PER_SECOND > clientModificationDate.getTime();
}
catch (ParseException ex) {
log.warn("Failed to parse value '{}' of If-Modified-Since header.", ifModifiedSince, ex);
}
}
// if resource wasn't modified: send a 304 and return true so the caller knows it shouldn't go on writing the response
if (!isModified) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return true;
}
// set last modified header so future requests can be cached
if (lastModificationDate != null) {
response.setHeader(HEADER_LAST_MODIFIED, formatDate(lastModificationDate));
if (setExpiresHeader) {
// by setting an expires header we force the browser to always check for updated versions (only on author)
response.setHeader(HEADER_EXPIRES, "-1");
}
}
// tell the caller it should go on writing the response as no 304-header was send
return false;
}
/**
* Set headers to disallow caching in browser, proxy servers and dispatcher for the current response.
* @param response Current response
*/
public static void setNonCachingHeaders(@NotNull HttpServletResponse response) {
response.setHeader(HEADER_PRAGMA, NO_CACHE);
response.setHeader(HEADER_CACHE_CONTROL, NO_CACHE);
response.setHeader(HEADER_EXPIRES, "0");
response.setHeader(HEADER_DISPATCHER, NO_CACHE);
}
/**
* Set expires header to given date.
* @param response Response
* @param date Expires date
*/
public static void setExpires(@NotNull HttpServletResponse response, @Nullable Date date) {
if (date == null) {
response.setHeader(HEADER_EXPIRES, "-1");
}
else {
response.setHeader(HEADER_EXPIRES, formatDate(date));
}
}
/**
* Set expires header to given amount of seconds in the future.
* @param response Response
* @param seconds Seconds to expire
*/
public static void setExpiresSeconds(@NotNull HttpServletResponse response, int seconds) {
Date expiresDate = DateUtils.addSeconds(new Date(), seconds);
setExpires(response, expiresDate);
}
/**
* Set expires header to given amount of hours in the future.
* @param response Response
* @param hours Hours to expire
*/
public static void setExpiresHours(@NotNull HttpServletResponse response, int hours) {
Date expiresDate = DateUtils.addHours(new Date(), hours);
setExpires(response, expiresDate);
}
/**
* Set expires header to given amount of days in the future.
* @param response Response
* @param days Days to expire
*/
public static void setExpiresDays(@NotNull HttpServletResponse response, int days) {
Date expiresDate = DateUtils.addDays(new Date(), days);
setExpires(response, expiresDate);
}
}