
com.github.jknack.mwa.wro4j.WroBaseModule Maven / Gradle / Ivy
package com.github.jknack.mwa.wro4j;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.commons.lang3.Validate.notNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.http.ConfigurableWroFilter;
import ro.isdc.wro.http.WroFilter;
import ro.isdc.wro.manager.factory.BaseWroManagerFactory;
import ro.isdc.wro.manager.factory.WroManagerFactory;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.factory.WroModelFactory;
import ro.isdc.wro.model.factory.WroModelFactoryDecorator;
import ro.isdc.wro.model.factory.XmlModelFactory;
import ro.isdc.wro.model.group.DefaultGroupExtractor;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.group.GroupExtractor;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.locator.ClasspathUriLocator;
import ro.isdc.wro.model.resource.locator.UrlUriLocator;
import ro.isdc.wro.model.resource.locator.factory.SimpleUriLocatorFactory;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.locator.wildcard.WildcardUriLocatorSupport;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.model.resource.processor.factory.ProcessorsFactory;
import ro.isdc.wro.model.resource.processor.factory.SimpleProcessorsFactory;
import ro.isdc.wro.model.transformer.WildcardExpanderModelTransformer.NoMoreAttemptsIOException;
import ro.isdc.wro.util.ObjectFactory;
import ro.isdc.wro.util.Transformer;
import com.github.jknack.mwa.Beans;
import com.github.jknack.mwa.FilterMapping;
import com.github.jknack.mwa.Mode;
import com.github.jknack.mwa.ModeAware;
import com.github.jknack.mwa.ModeCallback;
import com.github.jknack.mwa.mvc.MvcModule;
/**
*
* The {@link WroBaseModule} configure all the necessary infrastructure required by the WebResourceOptimizer
*
*
* Features:
*
*
* - Intercept all the *.js and *.css request and apply the all the registered {@link Processors
* processors}.
*
- The wro file descriptor can be written in: 'xml', 'groovy' or 'json'.
*
- While running in 'dev', the resource is treated as a single file (no group)
*
- While running in 'dev', a HTML is printed if a js or css file doesn't follow the rules of
* jsHint, jsLint or cssLint.
*
- While running in 'NO-dev', a group of files can be merged, minified and compressed as a
* single bundle.
*
*
* Please see the {@link Processors processors} for a full list of processors.
*
*
* @author edgar.espina
* @since 0.1.2
* @see Processors
*/
@Configuration
@Import(MvcModule.class)
public class WroBaseModule {
/**
* This class create a group for every single resource in a {@link WroModel}.
* This service is available in dev mode.
*
* @author edgar.espina
*/
@Profile("dev")
private static class GroupPerFileModel extends DefaultGroupExtractor
implements Transformer {
/**
* {@inheritDoc}
*/
@Override
public WroModel transform(final WroModel input) throws Exception {
Collection groups = input.getGroups();
Map> map = new HashMap>();
for (Group group : groups) {
List resources = group.getResources();
for (Resource resource : resources) {
String groupName = fileToGroup(resource.getUri());
Set newGroup = map.get(groupName);
if (newGroup == null) {
newGroup = new HashSet();
map.put(groupName, newGroup);
}
newGroup.add(resource);
}
// Keep the original group
map.put(group.getName(), new LinkedHashSet(resources));
}
WroModel output = new WroModel();
for (Entry> g : map.entrySet()) {
Group newGroup = new Group(g.getKey());
Set resources = g.getValue();
for (Resource resource : resources) {
newGroup.addResource(resource);
}
output.addGroup(newGroup);
}
return output;
}
/**
* {@inheritDoc}
*/
@Override
public String getGroupName(final HttpServletRequest request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be NULL!");
}
String uri = request.getRequestURI();
String contextPath = request.getContextPath();
if (uri.startsWith(contextPath)) {
// Strip contextPath if present
uri = uri.substring(contextPath.length());
}
final String groupName = fileToGroup(uri);
return StringUtils.isEmpty(groupName) ? null : groupName;
}
/**
* Convert the given file's name to a wro group's name.
*
* @param filename The file's name.
* @return The group's name.
*/
private static String fileToGroup(final String filename) {
String group = filename;
if (group.startsWith("/")) {
group = group.substring(1);
}
group = FilenameUtils.removeExtension(group);
return group.replace("/", "_");
}
}
/**
* A custom {@link WroConfigurationFactory}. Required by wro4j.
*
* @author edgar.espina
* @since 0.1.2
*/
private static class WroConfigurationFactory implements
ObjectFactory {
/**
* The {@link WroConfiguration}.
*/
private WroConfiguration configuration;
/**
* Creates a new {@link WroConfigurationFactory}.
*
* @param configuration The {@link WroConfiguration}. Required.
*/
public WroConfigurationFactory(final WroConfiguration configuration) {
this.configuration =
checkNotNull(configuration, "The wroConfiguration is required.");
}
/**
* {@inheritDoc}
*/
@Override
public WroConfiguration create() {
return configuration;
}
}
/**
* A {@link WroFilter} that handle exception and print html report while
* running in 'dev'.
*
* @author edgar.espina
* @since 0.1.2
*/
private static class ExtendedWroFilter extends ConfigurableWroFilter {
/**
* The {@link WroConfigurationFactory}.
*/
private WroConfigurationFactory configurationFactory;
/**
* The {@link WroConfiguration}.
*/
private WroConfiguration configuration;
/**
* Creates a new {@link ExtendedWroFilter}.
*
* @param configuration The {@link WroConfiguration}. Required.
* @param wroManagerFactory The wroManagerFactory. Required.
*/
public ExtendedWroFilter(final WroConfiguration configuration,
final WroManagerFactory wroManagerFactory) {
this.configuration =
checkNotNull(configuration, "The wroConfiguration is required.");
configurationFactory = new WroConfigurationFactory(configuration);
setWroManagerFactory(wroManagerFactory);
}
/**
* {@inheritDoc}
*/
@Override
protected ObjectFactory newWroConfigurationFactory(
final FilterConfig filterConfig) {
return configurationFactory;
}
/**
* Handle the exception by printing a HTML page in 'dev' mode. {@inheritDoc}
*/
@Override
protected void onException(final Exception ex,
final HttpServletResponse response, final FilterChain chain) {
if (configuration.isDebug()) {
HttpServletRequest request = Context.get().getRequest();
WroProblemReporter reporter = WroProblemReporter.bestFor(ex);
if (reporter != null) {
reporter.report(ex, request, response);
} else {
super.onException(ex, response, chain);
}
} else {
super.onException(ex, response, chain);
}
}
}
/**
* A custom implementation of the servlet context uri locator that doesn't
* depend on the wro-context object.
*
* @author edgar.espina
* @since 0.1.3
*/
private class ServletContextUriLocator extends WildcardUriLocatorSupport {
/**
* The prefix uri.
*/
private static final String PREFIX = "/";
/**
* The servelt context.
*/
private final ServletContext servletContext;
/**
* The logging system.
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Creates a new {@link ServletContextUriLocator}.
*
* @param servletContext The servlet context. Required.
*/
public ServletContextUriLocator(final ServletContext servletContext) {
this.servletContext =
checkNotNull(servletContext, "The servletContext is required.");
}
/**
* {@inheritDoc}
*/
@Override
public InputStream locate(final String uri) throws IOException {
try {
if (getWildcardStreamLocator().hasWildcard(uri)) {
final String fullPath = FilenameUtils.getFullPath(uri);
final String realPath = servletContext.getRealPath(fullPath);
if (realPath == null) {
final String message =
"Could not determine realPath for resource: " + uri;
logger.error(message);
throw new IOException(message);
}
return getWildcardStreamLocator().locateStream(uri,
new File(realPath));
}
} catch (NoMoreAttemptsIOException ex) {
throw ex;
} catch (IOException ex) {
logger
.warn(
"Couldn't localize the stream containing wildcard. Original "
+ "error message: '{}'", ex.getMessage()
+ "\".\n Trying to locate the stream without the "
+ "wildcard.");
}
InputStream input = servletContext.getResourceAsStream(uri);
Validate.notNull(input, "Resource not found: " + uri);
return input;
}
/**
* {@inheritDoc}
*/
@Override
public boolean accept(final String uri) {
return uri.trim().startsWith(PREFIX);
}
}
/**
* Look for wild-card expressions inside resources and resolved them using
* Spring.
*
* @author edgar.espina
* @since 0.2.6
*/
private static class WildcardModelFactory extends WroModelFactoryDecorator {
/**
* The logging system.
*/
private static final Logger logger =
LoggerFactory.getLogger(WildcardModelFactory.class);
/**
* The application's context.
*/
private ApplicationContext applicationContext;
/**
* A lock.
*/
private final Object lock = new Object();
/**
* A cached model for no-dev environment.
*/
private WroModel expanded = null;
/**
* The application's mode.
*/
private Mode mode;
/**
* Creates new {@link WildcardModelFactory}.
*
* @param applicationContext The application's context. Required.
* @param decorated The source model factory.
*/
public WildcardModelFactory(
final ApplicationContext applicationContext,
final WroModelFactory decorated) {
super(decorated);
this.applicationContext =
notNull(applicationContext, "The application's context is required.");
mode = applicationContext.getBean(Mode.class);
}
@Override
public WroModel create() {
if (mode.isDev()) {
return expand(super.create());
} else {
synchronized (lock) {
if (expanded == null) {
expanded = expand(super.create());
}
}
return expanded;
}
}
/**
* Expand the model if need it.
*
* @param model The wroModel
* @return The same model.
*/
private WroModel expand(final WroModel model) {
for (Group group : model.getGroups()) {
List resourceList = new ArrayList();
boolean expanded = false;
for (Resource resource : group.getResources()) {
String uri = resource.getUri();
try {
if (uri.contains("*") || uri.contains("?")) {
// A wild card was found.
String[] uriList = findResources(resource.getUri());
if (uriList.length > 0) {
logger.debug("Expanding {}: {}", group.getName(), uri);
expanded = true;
for (String newURI : uriList) {
logger.debug(" to: {}", newURI);
resourceList.add(Resource.create(newURI, resource.getType()));
}
}
} else {
resourceList.add(resource);
}
} catch (IOException ex) {
throw new WroRuntimeException("Resource " + uri
+ " cannot be processed.", ex);
}
}
if (expanded) {
// Override everything.
group.setResources(resourceList);
}
}
return model;
}
/**
* Find all the resources that matches the given wildcard expression.
*
* @param uri The wild-card expression.
* @return All the uri resources associated.
* @throws IOException If a resource cannot be read.
*/
private String[] findResources(final String uri) throws IOException {
try {
org.springframework.core.io.Resource[] resources =
applicationContext.getResources(uri);
if (resources == null || resources.length == 0) {
return new String[0];
}
String[] uris = new String[resources.length];
for (int i = 0; i < uris.length; i++) {
org.springframework.core.io.Resource resource = resources[i];
if (resource instanceof ClassPathResource) {
uris[i] = ((ClassPathResource) resource).getPath();
} else if (resource instanceof ServletContextResource) {
uris[i] = ((ServletContextResource) resource).getPath();
} else if (resource instanceof FileSystemResource) {
uris[i] = ((FileSystemResource) resource).getPath();
} else {
uris[i] = resource.getURL().toString();
}
}
return uris;
} catch (FileNotFoundException ex) {
logger.debug("Ignoring: {}. Reason: {}", uri, ex);
return new String[0];
}
}
}
/**
* Intercept js and css resource and apply all the registered {@link Processors}.
*
* @param filter The wro filter. Required.
* @return An interceptor for js and css resources.
*/
@Bean
public FilterMapping wroFilterMapping(final WroFilter filter) {
return FilterMapping.filter(
"/**/*.js",
"/**/*.css",
"**/*.js",
"**/*.css"
).through(filter);
}
/**
* Intercept js and css resource and apply all the registered {@link Processors}.
*
* @param configuration The {@link WroConfiguration}. Required.
* @param wroManagerFactory The {@link WroModelFactory}. Required.
* @return An interceptor for js and css resources.
*/
@Bean
public WroFilter wroFilter(final WroConfiguration configuration,
final WroManagerFactory wroManagerFactory) {
return new ExtendedWroFilter(configuration, wroManagerFactory);
}
/**
* Creates a {@link WroConfiguration} using the application's environment.
*
* @param env The application's environment. Required.
* @param mode The application's mode. Required.
* @return A {@link WroConfiguration} using the application's environment.
*/
@Bean
public WroConfiguration wroConfiguration(final Environment env,
final Mode mode) {
checkNotNull(env, "The application's environment is required.");
boolean debug = mode.isDev();
boolean disableCache = true;
boolean gzipEnabled = false;
boolean cacheGzippedContent = false;
long cacheUpdatePeriod = 0;
long modelUpdatePeriod = 1;
boolean parallelPreprocessing = false;
if (!debug) {
cacheGzippedContent = true;
gzipEnabled = true;
disableCache = false;
modelUpdatePeriod = 0;
}
WroConfiguration configuration = new WroConfiguration();
configuration.setCacheGzippedContent(env.getProperty(
"wro.cacheGzippedContent", Boolean.class, cacheGzippedContent));
configuration.setCacheUpdatePeriod(env.getProperty(
"wro.cacheUpdatePeriod", Long.class, cacheUpdatePeriod));
configuration.setConnectionTimeout(env.getProperty(
"wro.connectionTimeout", Integer.class,
WroConfiguration.DEFAULT_CONNECTION_TIMEOUT));
configuration.setDebug(debug);
configuration.setDisableCache(env.getProperty(
"wro.disableCache", Boolean.class, disableCache));
configuration.setEncoding(env.getProperty(
"wro.encoding", String.class, WroConfiguration.DEFAULT_ENCODING));
configuration.setGzipEnabled(env.getProperty(
"wro.gzipEnabled", Boolean.class, gzipEnabled));
configuration.setHeader(env.getProperty("wro.header"));
configuration.setIgnoreEmptyGroup(env.getProperty(
"wro.ignoreEmptyGroup", Boolean.class, true));
configuration.setIgnoreMissingResources(env.getProperty(
"wro.ignoreMissingResources", Boolean.class, true));
configuration.setJmxEnabled(env.getProperty(
"wro.jmxEnabled", Boolean.class, false));
configuration.setMbeanName(env.getProperty("wro.mbeanName"));
configuration.setModelUpdatePeriod(env.getProperty(
"wro.modelUpdatePeriod", Long.class, modelUpdatePeriod));
configuration.setParallelPreprocessing(env.getProperty(
"wro.parallelPreprocessing", Boolean.class, parallelPreprocessing));
return configuration;
}
/**
* Creates a new {@link UriLocatorFactory} service. Web resources can be on
* the classpath, web path or in remote location.
*
* @param servletContext The servlet context. Required.
* @return A new {@link UriLocatorFactory} service. Web resources can be on
* the classpath, web path or in remote location.
* @see ServletContextUriLocator
* @see ClasspathUriLocator
* @see UrlUriLocatorl
*/
@Bean
public UriLocatorFactory wroUriLocatorFactory(
final ServletContext servletContext) {
return new SimpleUriLocatorFactory()
.addUriLocator(new ServletContextUriLocator(servletContext))
.addUriLocator(new ClasspathUriLocator())
.addUriLocator(new UrlUriLocator());
}
/**
* Publish a {@link BaseWroManagerFactory}.
*
* @param applicationContext The application context. Required.
* @param environment The application's environment. Required.
* @param mode The application's mode. Required.
* @param wroModelFactory The wro model factory. Required.
* @param processorsFactory The user-defined {@link ProcessorsFactory}.
* Required.
* @param uriLocatorFactory The {@link UriLocatorFactory}. Required.
* @return A new {@link BaseWroManagerFactory}.
*/
@SuppressWarnings({"unchecked", "rawtypes" })
@Bean
public BaseWroManagerFactory wroManagerFactory(
final ApplicationContext applicationContext,
final Environment environment, final Mode mode,
final WroModelFactory wroModelFactory,
final ProcessorsFactory processorsFactory,
final UriLocatorFactory uriLocatorFactory) {
notNull(applicationContext, "The application's context is required.");
notNull(environment, "The application's environment is required.");
notNull(mode, "The application's descriptor is required.");
notNull(processorsFactory, "The processor's factory is required.");
notNull(uriLocatorFactory, "The uri locator factory is required.");
final BaseWroManagerFactory wroManagerFactory = new BaseWroManagerFactory();
wroManagerFactory
.setUriLocatorFactory(uriLocatorFactory)
.setModelFactory(new WildcardModelFactory(
applicationContext, wroModelFactory));
List transformers =
Beans.lookFor(applicationContext, Transformer.class);
GroupExtractor groupExtractor =
Beans.get(applicationContext, GroupExtractor.class);
boolean useDefaults =
transformers.size() == 0 && groupExtractor == null;
if (useDefaults) {
mode.execute(new ModeCallback
© 2015 - 2025 Weber Informatics LLC | Privacy Policy