/*
* Copyright (c) 2012, Inversoft Inc., All Rights Reserved
*
* 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.
*/
package org.primeframework.mvc.workflow;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.Calendar;
import org.primeframework.mvc.config.MVCConfiguration;
import org.primeframework.mvc.servlet.ServletTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
/**
* This class handles static resources via the Prime workflow chain. In order to handle resources that will be coming
* from JAR files and the web application directories, this first checks the incoming request URI against a configured
* set of prefixes. This set of prefixes is controlled using the configuration interface. The default list of prefixes
* is:
* <p/>
* <pre>
* /static
* </pre>
* <p/>
* This workflow can also be turned off completely using the configuration interface.
*
* @author Brian Pontarelli
*/
public class StaticResourceWorkflow implements Workflow {
private static final Logger logger = LoggerFactory.getLogger(StaticResourceWorkflow.class);
private final ServletContext context;
private final HttpServletRequest request;
private final HttpServletResponse response;
private final String[] staticPrefixes;
private final boolean enabled;
@Inject
public StaticResourceWorkflow(ServletContext context, HttpServletRequest request,
HttpServletResponse response, MVCConfiguration configuration) {
this.context = context;
this.request = request;
this.response = response;
this.staticPrefixes = configuration.staticResourcePrefixes();
this.enabled = configuration.staticResourcesEnabled();
}
/**
* Checks for static resource request and if it is one, locates and sends back the static resource. If it isn't one,
* it passes control down the chain.
*
* @param workflowChain The workflow chain to use if the request is not a static resource.
* @throws IOException If the request is a static resource and sending it failed or if the chain throws an
* IOException.
* @throws ServletException If the chain throws.
*/
public void perform(WorkflowChain workflowChain) throws IOException, ServletException {
boolean handled = false;
if (enabled) {
// Ensure that this is a request for a resource like foo.jpg
String uri = ServletTools.getRequestURI(request);
int dot = uri.lastIndexOf('.');
int slash = dot >= 0 ? uri.indexOf('/', dot) : 1;
if (slash == -1 && !uri.endsWith(".class")) {
for (String staticPrefix : staticPrefixes) {
if (uri.startsWith(staticPrefix)) {
handled = findStaticResource(uri, request, response);
}
}
}
}
if (!handled) {
workflowChain.continueWorkflow();
}
}
/**
* Locate a static resource and copy directly to the response, setting the appropriate caching headers.
*
* @param uri The resource uri.
* @param request The request
* @param response The response
* @return True if the resource was found in the classpath and if it was successfully written back to the output
* stream. Otherwise, this returns false if the resource doesn't exist in the classpath.
* @throws IOException If anything goes wrong
*/
protected boolean findStaticResource(String uri, HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (context.getResource(uri) != null) {
// It is in the webapp, just return false and let the container deal with it
return false;
}
// check for if-modified-since, prior to any other headers
long ifModifiedSince = 0;
try {
ifModifiedSince = request.getDateHeader("If-Modified-Since");
} catch (Exception e) {
logger.warn("Invalid If-Modified-Since header value [" + request.getHeader("If-Modified-Since") + "], ignoring");
}
if (ifModifiedSince > 0) {
// not modified, content is not sent - only basic headers and status SC_NOT_MODIFIED
response.setDateHeader("Expires", Long.MAX_VALUE);
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return true;
}
InputStream is = findInputStream(uri);
if (is == null) {
return false;
}
// Set the content-type header
String contentType = getContentType(uri);
if (contentType != null) {
response.setContentType(contentType);
}
// Components manage caching based on version numbers, so we can cache forever
Calendar now = Calendar.getInstance();
response.setDateHeader("Date", now.getTimeInMillis());
response.setDateHeader("Expires", Long.MAX_VALUE);
response.setDateHeader("Retry-After", Long.MAX_VALUE);
response.setHeader("Cache-Control", "public");
response.setDateHeader("Last-Modified", 0);
try {
ServletOutputStream sos = response.getOutputStream();
// Then output the file
byte[] b = new byte[8192];
int len;
do {
len = is.read(b);
if (len > 0) {
sos.write(b, 0, len);
}
} while (len != -1);
sos.flush();
} finally {
is.close();
}
return true;
}
/**
* Look for a static resource in the classpath.
*
* @param uri The resource URI.
* @return The inputstream of the resource.
* @throws IOException If there is a problem locating the resource.
*/
protected InputStream findInputStream(String uri) throws IOException {
uri = URLDecoder.decode(uri, "UTF-8");
if (uri.startsWith("/")) {
uri = uri.substring(1);
}
return Thread.currentThread().getContextClassLoader().getResourceAsStream(uri);
}
/**
* Determine the content type for the resource name.
*
* @param name The resource name.
* @return The mime type.
*/
protected String getContentType(String name) {
// NOT using the code provided activation.jar to avoid adding yet another dependency
// this is generally OK, since these are the main files we server up
if (name.endsWith(".js")) {
return "text/javascript";
} else if (name.endsWith(".css")) {
return "text/css";
} else if (name.endsWith(".html")) {
return "text/html";
} else if (name.endsWith(".txt")) {
return "text/plain";
} else if (name.endsWith(".gif")) {
return "image/gif";
} else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
} else if (name.endsWith(".png")) {
return "image/png";
} else {
return null;
}
}
}