/*
* Copyright (C) 2011 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.gatein.portal.controller.resource;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
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.URL;
import java.util.Locale;
import java.util.Properties;
import org.exoplatform.commons.cache.future.FutureMap;
import org.exoplatform.commons.utils.I18N;
import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.commons.utils.Safe;
import org.exoplatform.portal.application.ResourceRequestFilter;
import org.exoplatform.portal.resource.GateInResourcesDeployer;
import org.exoplatform.web.ControllerContext;
import org.exoplatform.web.WebAppController;
import org.exoplatform.web.WebRequestHandler;
import org.exoplatform.web.controller.QualifiedName;
import org.gatein.common.io.IOTools;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.wci.ServletContainerFactory;
import org.gatein.wci.WebAppEvent;
import org.gatein.wci.WebAppLifeCycleEvent;
import org.gatein.wci.WebAppListener;
/**
* @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
*/
public class ResourceRequestHandler extends WebRequestHandler implements WebAppListener {
public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
public static final String LAST_MODIFIED = "Last-Modified";
public static final String SUPPORT_GATEIN_RESOURCES = "org.gatein.supports.gatein-resources.";
/** . */
private static String PATH = "META-INF/maven/org.gatein.portal/exo.portal.component.web.resources/pom.properties";
/** . */
private static final Logger log = LoggerFactory.getLogger(ResourceRequestHandler.class);
/** . */
public static final String VERSION;
/** . */
public static final String SCRIPT_HANDLER_NAME = "script";
/**
* {@code Cache-Control} HTTP header
*/
public static final String CACHE_CONTROL = "Cache-Control";
/**
* Equivalent to {@code "max-age=" + MAX_AGE + ",s-maxage=" + MAX_AGE} where {@code MAX_AGE} is
* age in seconds.
*/
public static final String CACHE_CONTROL_VALUE;
static {
// Detecting version from maven properties
// empty value is ok
String version = "";
String property = PropertyManager.getProperty("gatein.assets.version");
if (property != null && !property.isEmpty()) {
version = property;
} else {
URL url = ResourceRequestHandler.class.getClassLoader().getResource(PATH);
if (url != null) {
log.debug("Loading resource serving version from " + url);
InputStream in = null;
try {
in = url.openStream();
Properties props = new Properties();
props.load(in);
version = props.getProperty("version");
} catch (IOException e) {
log.error("Could not read properties from " + url, e);
} finally {
IOTools.safeClose(in);
}
}
}
//
log.info("Use version \"" + version + "\" for resource serving");
VERSION = version;
long seconds = 86400;
String propValue = PropertyManager.getProperty("gatein.assets.script.max-age");
if (propValue != null) {
try {
seconds = Long.valueOf(propValue);
} catch (NumberFormatException e) {
log.warn("The gatein.assets.script.max-age property is not set properly.");
}
}
CACHE_CONTROL_VALUE = "max-age=" + seconds + ",s-maxage=" + seconds;
}
/** . */
public static final QualifiedName VERSION_QN = QualifiedName.create("gtn", "version");
/** . */
public static final QualifiedName RESOURCE_QN = QualifiedName.create("gtn", "resource");
/** . */
public static final QualifiedName SCOPE_QN = QualifiedName.create("gtn", "scope");
/** . */
public static final QualifiedName COMPRESS_QN = QualifiedName.create("gtn", "compress");
/** . */
public static final QualifiedName ORIENTATION_QN = QualifiedName.create("gtn", "orientation");
/** . */
public static final QualifiedName LANG_QN = QualifiedName.create("gtn", "lang");
/**
* Returns {@code true} if {@link PropertyManager#isDevelopping()} is {@code true} or
* if {@code ifModifiedSince} is before {@code lastModified}.
*
* {@code lastModified} is expected to be rounded down to the nearest second already before
* calling this method.
*
* @param ifModifiedSince the request header value
* @param lastModified the last modified date of a server side resource rounded down to the nearest second
* @return
*/
public static boolean isModified(long ifModifiedSince, long lastModified) {
if (PropertyManager.isDevelopping()) {
return true;
} else {
return lastModified > ifModifiedSince;
}
}
/** . */
private final FutureMap<ScriptKey, ScriptResult, ControllerContext> cache;
public ResourceRequestHandler() {
this.cache = new FutureMap<ScriptKey, ScriptResult, ControllerContext>(new ScriptLoader());
}
@Override
public String getHandlerName() {
return SCRIPT_HANDLER_NAME;
}
@Override
public boolean execute(ControllerContext context) throws Exception {
String resourceParam = context.getParameter(RESOURCE_QN);
String scopeParam = context.getParameter(SCOPE_QN);
//
if (scopeParam != null && resourceParam != null) {
String compressParam = context.getParameter(COMPRESS_QN);
String lang = context.getParameter(LANG_QN);
//
Locale locale = null;
if (lang != null && lang.length() > 0) {
locale = I18N.parseTagIdentifier(lang);
}
//
ResourceScope scope;
try {
scope = ResourceScope.valueOf(ResourceScope.class, scopeParam);
} catch (IllegalArgumentException e) {
HttpServletResponse response = context.getResponse();
String msg = "Unrecognized scope " + scopeParam;
log.error(msg);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
return true;
}
//
ResourceId resource = new ResourceId(scope, resourceParam);
ScriptKey key = new ScriptKey(resource, "min".equals(compressParam), locale);
//
ScriptResult result = cache.get(context, key);
HttpServletResponse response = context.getResponse();
HttpServletRequest request = context.getRequest();
//
if (result instanceof ScriptResult.Resolved) {
ScriptResult.Resolved resolved = (ScriptResult.Resolved) result;
// Content type + charset
response.setContentType("text/javascript");
response.setCharacterEncoding("UTF-8");
response.setHeader(CACHE_CONTROL, CACHE_CONTROL_VALUE);
// Set content length
response.setContentLength(resolved.bytes.length);
long ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE);
if (isModified(ifModifiedSince, resolved.lastModified)) {
response.setDateHeader(ResourceRequestFilter.LAST_MODIFIED, resolved.lastModified);
// Send bytes
ServletOutputStream out = response.getOutputStream();
try {
out.write(resolved.bytes);
} finally {
Safe.close(out);
}
} else {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
} else if (result instanceof ScriptResult.Error) {
ScriptResult.Error error = (ScriptResult.Error) result;
log.error("Could not render script " + key + "\n:" + error.message);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} else {
String msg = "Resource " + key + " cannot be found";
log.error(msg);
response.sendError(HttpServletResponse.SC_NOT_FOUND, msg);
}
} else {
HttpServletResponse response = context.getResponse();
String msg = "Missing scope or resource param";
log.error(msg);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
//
return true;
}
@Override
protected boolean getRequiresLifeCycle() {
return false;
}
@Override
public void onInit(WebAppController controller, ServletConfig sConfig) throws Exception {
super.onInit(controller, sConfig);
log.debug("Registering ResourceRequestHandler for servlet container events");
ServletContainerFactory.getServletContainer().addWebAppListener(this);
}
@Override
public void onDestroy(WebAppController controller) {
super.onDestroy(controller);
log.debug("Unregistering ResourceRequestHandler for servlet container events");
ServletContainerFactory.getServletContainer().removeWebAppListener(this);
}
@Override
public void onEvent(WebAppEvent event) {
if (event instanceof WebAppLifeCycleEvent) {
WebAppLifeCycleEvent lifeCycleEvent = (WebAppLifeCycleEvent) event;
ServletContext servletContext = lifeCycleEvent.getWebApp().getServletContext();
if (WebAppLifeCycleEvent.ADDED == lifeCycleEvent.getType()) {
InputStream is = servletContext.getResourceAsStream(GateInResourcesDeployer.GATEIN_CONFIG_RESOURCE);
if (is != null) {
servletContext.setAttribute(SUPPORT_GATEIN_RESOURCES, true);
}
} else if (servletContext.getAttribute(SUPPORT_GATEIN_RESOURCES) != null
&& WebAppLifeCycleEvent.REMOVED == lifeCycleEvent.getType()) {
cache.clear();
servletContext.removeAttribute(SUPPORT_GATEIN_RESOURCES);
}
}
}
}