/*
* Copyright (c) 2008. All rights reserved.
*/
package ro.isdc.wro.http;
import static org.apache.commons.lang3.Validate.notNull;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.Collection;
import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.config.factory.PropertiesAndFilterConfigWroConfigurationFactory;
import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.http.handler.RequestHandler;
import ro.isdc.wro.http.handler.factory.DefaultRequestHandlerFactory;
import ro.isdc.wro.http.handler.factory.RequestHandlerFactory;
import ro.isdc.wro.http.support.ResponseHeadersConfigurer;
import ro.isdc.wro.http.support.ServletContextAttributeHelper;
import ro.isdc.wro.manager.factory.DefaultWroManagerFactory;
import ro.isdc.wro.manager.factory.WroManagerFactory;
import ro.isdc.wro.model.group.processor.Injector;
import ro.isdc.wro.model.group.processor.InjectorBuilder;
import ro.isdc.wro.model.resource.locator.ServletContextUriLocator;
import ro.isdc.wro.model.resource.locator.support.DispatcherStreamLocator;
import ro.isdc.wro.util.ObjectFactory;
import ro.isdc.wro.util.WroUtil;
/**
* Main entry point. Perform the request processing by identifying the type of the requested resource. Depending on the
* way it is configured.
*
* @author Alex Objelean
* @created Created on Oct 31, 2008
*/
public class WroFilter
implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(WroFilter.class);
/**
* The prefix to use for default mbean name.
*/
private static final String MBEAN_PREFIX = "wro4j-";
/**
* Attribute indicating that the request was passed through {@link WroFilter}. This is required to allow identify
* requests for wro resources (example: async resourceWatcher which cannot be executed asynchronously unless a wro
* resource was requested).
*
* @VisibleForTesting
*/
public static final String ATTRIBUTE_PASSED_THROUGH_FILTER = WroFilter.class.getName()
+ ".passed_through_filter";
/**
* Filter config.
*/
private FilterConfig filterConfig;
private ObjectFactory<WroConfiguration> wroConfigurationFactory;
/**
* Wro configuration.
*/
private WroConfiguration wroConfiguration;
/**
* WroManagerFactory. The core of the optimizer.
*/
private WroManagerFactory wroManagerFactory;
/**
* Used to create the collection of requestHandlers to apply
*/
private RequestHandlerFactory requestHandlerFactory;
private ResponseHeadersConfigurer headersConfigurer;
/**
* Flag used to toggle filter processing. When this flag is false, the filter will proceed with chaining. This flag is
* true by default.
*/
private boolean enable = true;
private Injector injector;
private MBeanServer mbeanServer = null;
/**
* @return true if the provided request contains an attribute indicating that it was handled through {@link WroFilter}
*/
public static boolean isPassedThroughyWroFilter(final HttpServletRequest request) {
notNull(request);
return request.getAttribute(ATTRIBUTE_PASSED_THROUGH_FILTER) != null;
}
public final void init(final FilterConfig config)
throws ServletException {
this.filterConfig = config;
// invoke createConfiguration method only if the configuration was not set.
this.wroConfiguration = createConfiguration();
this.wroManagerFactory = createWroManagerFactory();
this.injector = InjectorBuilder.create(wroManagerFactory).build();
headersConfigurer = newResponseHeadersConfigurer();
requestHandlerFactory = DefaultRequestHandlerFactory.decorate(newRequestHandlerFactory(), new ObjectFactory<Injector>() {
public Injector create() {
return getInjector();
}
});
registerChangeListeners();
registerMBean();
doInit(config);
LOG.info("wro4j version: {}", WroUtil.getImplementationVersion());
LOG.info("wro4j configuration: {}", wroConfiguration);
}
/**
* Creates configuration by looking up in servletContext attributes. If none is found, a new one will be created using
* the configuration factory.
*
* @return {@link WroConfiguration} object.
*/
private WroConfiguration createConfiguration() {
// Extract config from servletContext (if already configured)
// TODO use a named helper
final WroConfiguration configAttribute = ServletContextAttributeHelper.create(filterConfig).getWroConfiguration();
if (configAttribute != null) {
setConfiguration(configAttribute);
}
return getWroConfigurationFactory().create();
}
/**
* Creates {@link WroManagerFactory}.
*/
private WroManagerFactory createWroManagerFactory() {
if (wroManagerFactory == null) {
final WroManagerFactory managerFactoryAttribute = ServletContextAttributeHelper.create(filterConfig)
.getManagerFactory();
LOG.debug("managerFactory attribute: {}", managerFactoryAttribute);
wroManagerFactory = managerFactoryAttribute != null ? managerFactoryAttribute : newWroManagerFactory();
}
LOG.debug("created managerFactory: {}", wroManagerFactory);
return wroManagerFactory;
}
/**
* Expose MBean to tell JMX infrastructure about our MBean (only if jmxEnabled is true).
*/
private void registerMBean() {
if (wroConfiguration.isJmxEnabled()) {
try {
mbeanServer = getMBeanServer();
final ObjectName name = getMBeanObjectName();
if (!mbeanServer.isRegistered(name)) {
mbeanServer.registerMBean(wroConfiguration, name);
}
} catch (final JMException e) {
LOG.error("Exception occured while registering MBean", e);
}
}
}
private void unregisterMBean() {
try {
if (mbeanServer != null && mbeanServer.isRegistered(getMBeanObjectName())) {
mbeanServer.unregisterMBean(getMBeanObjectName());
}
} catch (final JMException e) {
LOG.error("Exception occured while registering MBean", e);
}
}
private ObjectName getMBeanObjectName()
throws MalformedObjectNameException {
return new ObjectName(newMBeanName(), "type", WroConfiguration.class.getSimpleName());
}
/**
* @return the name of MBean to be used by JMX to configure wro4j.
*/
protected String newMBeanName() {
String mbeanName = wroConfiguration.getMbeanName();
if (StringUtils.isEmpty(mbeanName)) {
final String contextPath = getContextPath();
mbeanName = StringUtils.isEmpty(contextPath) ? "ROOT" : contextPath;
mbeanName = MBEAN_PREFIX + mbeanName;
}
return mbeanName;
}
/**
* @return Context path of the application.
*/
private String getContextPath() {
String contextPath = null;
try {
contextPath = (String) ServletContext.class.getMethod("getContextPath", new Class<?>[] {}).invoke(
filterConfig.getServletContext(), new Object[] {});
} catch (final Exception e) {
contextPath = "DEFAULT";
LOG.warn("Couldn't identify contextPath because you are using older version of servlet-api (<2.5). Using "
+ contextPath + " contextPath.");
}
return contextPath.replaceFirst(ServletContextUriLocator.PREFIX, "");
}
/**
* Override this method if you want to provide a different MBeanServer.
*
* @return {@link MBeanServer} to use for JMX.
*/
protected MBeanServer getMBeanServer() {
return ManagementFactory.getPlatformMBeanServer();
}
/**
* Register property change listeners.
*/
private void registerChangeListeners() {
wroConfiguration.registerCacheUpdatePeriodChangeListener(new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent event) {
// reset cache headers when any property is changed in order to avoid browser caching
headersConfigurer = newResponseHeadersConfigurer();
wroManagerFactory.onCachePeriodChanged(valueAsLong(event.getNewValue()));
}
});
wroConfiguration.registerModelUpdatePeriodChangeListener(new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent event) {
headersConfigurer = newResponseHeadersConfigurer();
wroManagerFactory.onModelPeriodChanged(valueAsLong(event.getNewValue()));
}
});
LOG.debug("Cache & Model change listeners were registered");
}
/**
* @return the {@link ResponseHeadersConfigurer}.
*/
protected ResponseHeadersConfigurer newResponseHeadersConfigurer() {
return ResponseHeadersConfigurer.fromConfig(wroConfiguration);
}
/**
* @return default implementation of {@link RequestHandlerFactory}
*/
protected RequestHandlerFactory newRequestHandlerFactory() {
return new DefaultRequestHandlerFactory();
}
private long valueAsLong(final Object value) {
Validate.notNull(value);
return Long.valueOf(String.valueOf(value)).longValue();
}
/**
* Custom filter initialization - can be used for extended classes.
*
* @see Filter#init(FilterConfig).
*/
protected void doInit(final FilterConfig config)
throws ServletException {
}
public final void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
if (isFilterActive(request)) {
LOG.debug("processing wro request: {}", request.getRequestURI());
try {
// add request, response & servletContext to thread local
Context.set(Context.webContext(request, response, filterConfig), wroConfiguration);
addPassThroughFilterAttribute(request);
if (!handledWithRequestHandler(request, response)) {
processRequest(request, response);
onRequestProcessed();
}
} catch (final Exception e) {
onException(e, response, chain);
} finally {
Context.unset();
}
} else {
chain.doFilter(request, response);
}
}
private void addPassThroughFilterAttribute(final HttpServletRequest request) {
request.setAttribute(ATTRIBUTE_PASSED_THROUGH_FILTER, Boolean.TRUE);
}
private boolean handledWithRequestHandler(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
final Collection<RequestHandler> handlers = requestHandlerFactory.create();
notNull(handlers, "requestHandlers cannot be null!");
// create injector used for process injectable fields from each requestHandler.
for (final RequestHandler requestHandler : handlers) {
if (requestHandler.isEnabled() && requestHandler.accept(request)) {
requestHandler.handle(request, response);
return true;
}
}
return false;
}
/**
* @return {@link Injector} used to inject {@link RequestHandler}'s.
* @VisibleForTesting
*/
Injector getInjector() {
return injector;
}
/**
* Perform actual processing.
*/
private void processRequest(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
setResponseHeaders(response);
// process the uri using manager
wroManagerFactory.create().process();
}
/**
* @return true if the filter should be applied or proceed with chain otherwise.
*/
private boolean isFilterActive(final HttpServletRequest request) {
// prevent StackOverflowError by skipping the already included wro request
return enable && !DispatcherStreamLocator.isIncludedRequest(request);
}
/**
* Invoked when a {@link Exception} is thrown. Allows custom exception handling. The default implementation proceeds
* with filter chaining when exception is thrown.
*
* @param e
* {@link Exception} thrown during request processing.
*/
protected void onException(final Exception e, final HttpServletResponse response, final FilterChain chain) {
LOG.debug("Exception occured", e);
try {
LOG.debug("Cannot process. Proceeding with chain execution.");
chain.doFilter(Context.get().getRequest(), response);
} catch (final Exception ex) {
// should never happen (use debug level to suppress unuseful logs)
LOG.debug("Error while chaining the request", e);
}
}
/**
* Method called for each request and responsible for setting response headers, used mostly for cache control.
* Override this method if you want to change the way headers are set.<br>
*
* @param response
* {@link HttpServletResponse} object.
*/
protected void setResponseHeaders(final HttpServletResponse response) {
headersConfigurer.setHeaders(response);
}
/**
* Allows external configuration of {@link WroManagerFactory} (ex: using spring IoC). When this value is set, the
* default {@link WroManagerFactory} initialization won't work anymore.
* <p/>
* Note: call this method before {@link WroFilter#init(FilterConfig)} is invoked.
*
* @param wroManagerFactory
* the wroManagerFactory to set
*/
public void setWroManagerFactory(final WroManagerFactory wroManagerFactory) {
this.wroManagerFactory = wroManagerFactory;
}
/**
* @return configured and decorated {@link WroManagerFactory} instance.
*/
public final WroManagerFactory getWroManagerFactory() {
return this.wroManagerFactory;
}
/**
* Sets the RequestHandlerFactory used to create the collection of requestHandlers
*
* @param requestHandlerFactory
* to set
*/
public void setRequestHandlerFactory(final RequestHandlerFactory requestHandlerFactory) {
Validate.notNull(requestHandlerFactory);
this.requestHandlerFactory = requestHandlerFactory;
}
/**
* Factory method for {@link WroManagerFactory}.
* <p/>
* Creates a {@link WroManagerFactory} configured in {@link WroConfiguration} using reflection. When no configuration
* is found a default implementation is used.
* </p>
* Note: this method is not invoked during initialization if a {@link WroManagerFactory} is set using
* {@link WroFilter#setWroManagerFactory(WroManagerFactory)}.
*
* @return {@link WroManagerFactory} instance.
*/
protected WroManagerFactory newWroManagerFactory() {
return DefaultWroManagerFactory.create(wroConfigurationFactory);
}
/**
* @return implementation of {@link ObjectFactory<WroConfiguration>} used to create a {@link WroConfiguration} object.
*/
protected ObjectFactory<WroConfiguration> newWroConfigurationFactory(final FilterConfig filterConfig) {
return new PropertiesAndFilterConfigWroConfigurationFactory(filterConfig);
}
private ObjectFactory<WroConfiguration> getWroConfigurationFactory() {
if (wroConfigurationFactory == null) {
wroConfigurationFactory = newWroConfigurationFactory(filterConfig);
}
return wroConfigurationFactory;
}
public void setWroConfigurationFactory(final ObjectFactory<WroConfiguration> wroConfigurationFactory) {
this.wroConfigurationFactory = wroConfigurationFactory;
}
/**
* @return the {@link WroConfiguration} associated with this filter instance.
* @VisibleForTesting
*/
public final WroConfiguration getConfiguration() {
return this.wroConfiguration;
}
/**
* Once set, this configuration will be used, instead of the one built by the factory.
*
* @param config
* a not null {@link WroConfiguration} to set.
*/
public final void setConfiguration(final WroConfiguration config) {
Validate.notNull(config);
wroConfigurationFactory = new ObjectFactory<WroConfiguration>() {
public WroConfiguration create() {
return config;
}
};
}
/**
* Sets the enable flag used to toggle filter. This might be useful when the filter has to be enabled/disabled based
* on environment configuration.
*
* @param enable
* flag for enabling the {@link WroFilter}.
*/
public void setEnable(final boolean enable) {
this.enable = enable;
}
/**
* Useful for unit tests to check the post processing.
*/
protected void onRequestProcessed() {
}
/**
* {@inheritDoc}
*/
public void destroy() {
//Avoid memory leak by unregistering mBean on destroy
unregisterMBean();
if (wroManagerFactory != null) {
wroManagerFactory.destroy();
}
if (wroConfiguration != null) {
wroConfiguration.destroy();
}
Context.destroy();
}
}