/*
* $HeadURL$
* $Id$
* Copyright (c) 2006-2012 by Public Library of Science http://plos.org http://ambraproject.org
* 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.0Unless 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.
*/
package org.ambraproject.util;
import freemarker.core.Environment;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import org.ambraproject.web.VirtualJournalContext;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.ambraproject.configuration.ConfigurationStore;
import sun.misc.BASE64Encoder;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
/**
* Base class for templates that render links to static files. Subclasses can use the value of getFingerprint to
* modify the link, such that whenever the file changes, the link will also change. In this way, browser caching
* should function correctly--the browser will use the copy from its cache if and only if the file remains unchanged
* on the server.
* <p/>
* This class uses caching internally for performance, so a server restart is necessary when any static files
* referenced by subclasses are changed.
*/
public abstract class VersionedFileDirective implements TemplateDirectiveModel {
private static final Logger log = LoggerFactory.getLogger(VersionedFileDirective.class);
/**
* Frequency with which the fingerprintCache is cleared, in milliseconds. mbaehr said that 15 minutes is a
* value that is frequently used throughout our site.
*/
private static final int CACHE_PURGE_DELAY = 15 * 60 * 1000;
/**
* Cache used to store fingerprint values. We do this since it might significantly impact the performance of the
* server if we re-checksum every file every time the server gets a request for it.
* <p/>
* If the number of .js and .css files in the entire application becomes large, we might consider changing this
* to a cache that evicts elements, instead of keeping everything in memory.
*/
private Map<String, String> fingerprintCache = new ConcurrentHashMap<String, String>();
/**
* Timer used to clear out fingerprintCache occasionally. This is because we cannot assume that there will be a
* server restart when new .js or .css files are deployed.
*/
private Timer cachePurgeTimer;
public VersionedFileDirective() {
cachePurgeTimer = new Timer("fingerprintCache purging timer", true);
cachePurgeTimer.schedule(new TimerTask() {
@Override
public void run() {
fingerprintCache.clear();
log.info("timer task cleared fingerprintCache");
}
}, CACHE_PURGE_DELAY, CACHE_PURGE_DELAY);
}
/**
* Returns a base64-encoded fingerprint of the contents of a file.
*
* @param filepath the real filesystem path of the file being served
* @return base64-encoded fingerprint of the file's contents
* @throws IOException
* @throws TemplateException
*/
String getFingerprint(String filepath) throws IOException, TemplateException {
String cached = fingerprintCache.get(filepath);
if (cached != null) {
return cached;
}
byte[] buffer = IOUtils.toByteArray(new FileInputStream(filepath));
String fingerprint = TextUtils.createHash(buffer);
fingerprintCache.put(filepath, fingerprint);
return fingerprint;
}
/**
* {@inheritDoc}
*/
public void execute(Environment environment, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
if (params.get("file") == null) {
throw new TemplateModelException("file parameter is required");
}
String filename = params.get("file").toString();
// We need a ServletContext in order to convert application-base paths into real paths. It's a little
// cumbersome to get one from here.
HttpServletRequest request = ((HttpRequestHashModel) environment.getDataModel().get("Request")).getRequest();
String path;
try {
path = getRealPath(filename, request);
} catch (ServletException se) {
throw new TemplateModelException(se);
}
// There are some style and script tags in the codebase that refer to non-existent files. Just do nothing
// in these cases.
File test = new File(path);
if (test.exists()) {
environment.getOut().write(getLink(filename, getFingerprint(path), params));
}
}
/**
* Resolves the filesystem path based on a web request path. This is made complicated by
* {@link org.ambraproject.web.VirtualJournalMappingFilter}, which remaps certain paths to journal-specific paths.
*
* @param path The original web request path
* @param request The request object we're currently serving. Note that this request will have a different path
* than the path param.
* @return The path to the resource on the filesystem
* @throws ServletException
*/
private String getRealPath(final String path, HttpServletRequest request) throws ServletException {
VirtualJournalContext vjc =
(VirtualJournalContext) request.getAttribute(VirtualJournalContext.PUB_VIRTUALJOURNAL_CONTEXT);
ServletContext servletContext = request.getSession().getServletContext();
// This is somewhat of a hack. VirtualJournalContext.mapRequest was originally written to be called with an
// HttpServletRequest (supplied by VirtualJournalMappingFilter). To reuse the code, we create a fake request
// for the resource in question, populating only the fields that matter.
HttpServletRequest fakeRequest = new HttpServletRequestWrapper(request) {
public String getRequestURI() {
return path;
}
public String getContextPath() {
return "";
}
public String getServletPath() {
return path;
}
public String getPathInfo() {
return null;
}
};
Configuration configuration = ConfigurationStore.getInstance().getConfiguration();
HttpServletRequest mappedRequest = vjc.mapRequest(fakeRequest, configuration,
servletContext);
// If getPathInfo is null, the request was not remapped, and is intended to be served from the root context. In
// that case we can get the real path from the ServletContext.
return mappedRequest.getPathInfo() != null ? mappedRequest.getPathInfo() : servletContext.getRealPath(path);
}
/**
* Returns the link that will be rendered.
*
* @param filename the static file being served (relative to the webapp base context)
* @param fingerprint checksum of the file's contents
* @param params parameters passed to the directive
* @return HTML link to the static file. The exact form will be subclass-specific (for instance, a link tag for
* css or a script tag for javascript).
*/
public abstract String getLink(String filename, String fingerprint, Map params) throws TemplateException;
}