/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* 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 org.wisdom.resources;
import org.osgi.framework.Bundle;
import org.slf4j.LoggerFactory;
import org.wisdom.api.asset.Asset;
import org.wisdom.api.configuration.ApplicationConfiguration;
import org.wisdom.api.crypto.Crypto;
import org.wisdom.api.http.*;
import org.wisdom.api.utils.DateUtil;
import java.io.File;
import java.net.URL;
/**
* Some cache control utilities.
*/
public class CacheUtils {
/**
* Value to set max age in header. E.g. Cache-Control:max-age=XXXXXX.
*/
public static final String HTTP_CACHE_CONTROL_MAX_AGE = "http.cache_control_max_age";
/**
* Default value for Cache-Control http header when not set in application.conf.
*/
public static final String HTTP_CACHE_CONTROL_DEFAULT = "3600";
/**
* Enable / disable etag E.g. ETag:"f0680fd3".
*/
public static final String HTTP_USE_ETAG = "http.useETag";
/**
* Default value / etag enabled by default.
*/
public static final boolean HTTP_USE_ETAG_DEFAULT = true;
/**
* Add the last modified header to the given result. This method handle the HTTP Date format.
*
* @param result the result
* @param lastModified the date
*/
public static void addLastModified(Result result, long lastModified) {
result.with(HeaderNames.LAST_MODIFIED, DateUtil.formatForHttpHeader(lastModified));
}
/**
* Check whether the request can send a NOT_MODIFIED response.
*
* @param context the context
* @param lastModified the last modification date
* @param etag the etag.
* @return true if the content is modified
*/
public static boolean isNotModified(Context context, long lastModified, String etag) {
// First check etag. Important, if there is an If-None-Match header, we MUST not check the
// If-Modified-Since header, regardless of whether If-None-Match matches or not. This is in
// accordance with section 14.26 of RFC2616.
final String browserEtag = context.header(HeaderNames.IF_NONE_MATCH);
if (browserEtag != null) {
// We check the given etag against the given one.
// If the given one is null, that means that etags are disabled.
return browserEtag.equals(etag);
}
// IF_NONE_MATCH not set, check IF_MODIFIED_SINCE
final String ifModifiedSince = context.header(HeaderNames.IF_MODIFIED_SINCE);
if (ifModifiedSince != null && lastModified > 0 && !ifModifiedSince.isEmpty()) {
try {
// We do a double check here because the time granularity is important here.
// If the written date headers are still the same, we are unchanged (the granularity is the
// second).
return ifModifiedSince.equals(DateUtil.formatForHttpHeader(lastModified));
} catch (IllegalArgumentException ex) {
LoggerFactory.getLogger(CacheUtils.class)
.error("Cannot build the date string for {}", lastModified, ex);
return true;
}
}
return false;
}
/**
* Computes the ETAG value based on the last modification date passed as parameter.
*
* @param lastModification the last modification (must be valid)
* @param configuration the configuration
* @param crypto the crypto service
* @return the encoded etag
*/
public static String computeEtag(long lastModification, ApplicationConfiguration configuration, Crypto crypto) {
boolean useEtag = configuration.getBooleanWithDefault(HTTP_USE_ETAG,
HTTP_USE_ETAG_DEFAULT);
if (!useEtag) {
return null;
}
String raw = Long.toString(lastModification);
return crypto.hexSHA1(raw);
}
/**
* Adds cache control and etag to the given result.
*
* @param result the result
* @param etag the etag
* @param configuration the application configuration
*/
public static void addCacheControlAndEtagToResult(Result result, String etag, ApplicationConfiguration configuration) {
String maxAge = configuration.getWithDefault(HTTP_CACHE_CONTROL_MAX_AGE,
HTTP_CACHE_CONTROL_DEFAULT);
if ("0".equals(maxAge)) {
result.with(HeaderNames.CACHE_CONTROL, "no-cache");
} else {
result.with(HeaderNames.CACHE_CONTROL, "max-age=" + maxAge);
}
// Use etag on demand:
boolean useEtag = configuration.getBooleanWithDefault(HTTP_USE_ETAG,
HTTP_USE_ETAG_DEFAULT);
if (useEtag) {
result.with(HeaderNames.ETAG, etag);
}
}
/**
* Computes the result to sent the given file. Cache headers are automatically set by this method.
*
* @param file the file to send to the client
* @param context the context
* @param configuration the application configuration
* @param crypto the crypto service
* @return the result, it can be a NOT_MODIFIED if the file was not modified since the last request,
* or an OK result with the cache headers set.
*/
public static Result fromFile(File file, Context context, ApplicationConfiguration configuration, Crypto crypto) {
long lastModified = file.lastModified();
String etag = computeEtag(lastModified, configuration, crypto);
if (isNotModified(context, lastModified, etag)) {
return new Result(Status.NOT_MODIFIED);
} else {
Result result = Results.ok(file);
addLastModified(result, lastModified);
addCacheControlAndEtagToResult(result, etag, configuration);
return result;
}
}
public static Result fromBundle(Bundle bundle, URL url, Context context, ApplicationConfiguration configuration,
Crypto crypto) {
long lastModified = bundle.getLastModified();
String etag = CacheUtils.computeEtag(lastModified, configuration, crypto);
if (CacheUtils.isNotModified(context, lastModified, etag)) {
return new Result(Status.NOT_MODIFIED);
} else {
Result result = Results.ok(url);
addLastModified(result, lastModified);
addCacheControlAndEtagToResult(result, etag, configuration);
return result;
}
}
public static Result fromAsset(Context context, Asset asset, ApplicationConfiguration configuration) {
if (CacheUtils.isNotModified(context, asset.getLastModified(), asset.getEtag())) {
return new Result(Status.NOT_MODIFIED);
} else {
Result result;
if (asset.getContent() instanceof File) {
result = Results.ok((File) asset.getContent());
} else if (asset.getContent() instanceof URL) {
result = Results.ok((URL) asset.getContent());
} else {
// Use object, probably won't work.
result = Results.ok(asset.getContent());
}
addLastModified(result, asset.getLastModified());
addCacheControlAndEtagToResult(result, asset.getEtag(), configuration);
return result;
}
}
}