Permalink
Browse files

Add caching to CoffeeFilter, refactoring

  • Loading branch information...
raymyers committed Nov 1, 2011
1 parent 0816ee9 commit 72b849d9bdc9ec41a158b14a64c74a3088d849cb
View
@@ -6,5 +6,6 @@
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
<classpathentry exported="true" kind="con" path="com.springsource.sts.gradle.classpathcontainer"/>
+ <classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/>
<classpathentry kind="output" path="bin"/>
</classpath>
View
@@ -5,14 +5,27 @@
<projects>
</projects>
<buildSpec>
+ <buildCommand>
+ <name>org.eclipse.wst.common.project.facet.core.builder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
+ <buildCommand>
+ <name>org.eclipse.wst.validation.validationbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
</buildSpec>
<natures>
+ <nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
+ <nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>com.springsource.sts.gradle.core.nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>
View
@@ -1,6 +1,6 @@
# Java Coffeescript Extensions
-Utilities for Java projects using coffeescript
+Utilities for Java projects using coffeescript.
Features provided:
View
@@ -20,7 +20,7 @@ repositories {
dependencies {
compile group: 'commons-lang', name: 'commons-lang', version: '2.5'
- compile group: 'com.google.guava', name: 'guava', version: 'r06'
+ compile group: 'com.google.guava', name: 'guava', version: '10.0.1'
compile group: 'org.mozilla', name: 'rhino', version: '1.7R3'
compile group: 'javax.servlet', name: 'servlet-api', version: '2.5'
@@ -5,11 +5,6 @@
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
@@ -20,14 +15,16 @@
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
+import com.cadrlife.coffee.compile.CachingCoffeeCompiler;
import com.cadrlife.coffee.concat.CoffeescriptConcatenate;
-import com.cadrlife.coffee.jcoffeescript.JCoffeeScriptCompileException;
-import com.cadrlife.coffee.jcoffeescript.JCoffeeScriptCompiler;
+import com.cadrlife.coffee.internal.org.springframework.util.AntPathMatcher;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.io.CharStreams;
+import com.google.common.io.Closeables;
/**
* Filter to compile coffeescript on the fly, with concatenation support. Does
@@ -38,50 +35,30 @@
* coffeeFiles. Required. Ant-style path to all coffee files
* ex. /WEB-INF/js/*.coffee
*
- * concatenateRoot. Optional. Path to the root file to resolve dependencies and concatenate results
+ * concatenateRoot. Optional. Path to the root file to resolve dependencies and concatenate results.
+ * Must be exact path to a single file.
* ex. /WEB-INF/js/main.coffee
*
* concatenateName. Optional. Path that maps to the concatenated source code.
* ex. /js/app.js
*
- * NOT YET IMPLEMENTED: cacheUpdateSeconds. Optional. How often to force an update of the compiled coffeescript cache
- * ex. 600
*/
public class CoffeeFilter implements Filter {
private String concatenateRoot = "";
private String concatenateName = "";
private String coffeeFiles = "";
private boolean concatenationEnabled;
+ private AntPathMatcher antPathMatcher = new AntPathMatcher();
- private static final class CompiledCoffee {
- // public final Date dateCached; // Time this was put in the cache
- public final String output; // Compiled coffee
+ private CachingCoffeeCompiler compiler;
- public CompiledCoffee(Date date, String output) {
- // this.dateCached = date;
- this.output = output;
- }
- }
-
- // Regex to get the line number of the failure.
- private static final Pattern LINE_NUMBER = Pattern.compile("line ([0-9]+)");
- private static final ThreadLocal<JCoffeeScriptCompiler> compiler = new ThreadLocal<JCoffeeScriptCompiler>() {
- @Override
- protected JCoffeeScriptCompiler initialValue() {
- return new JCoffeeScriptCompiler();
- }
- };
- private Map<String, CompiledCoffee> cache; // Map of Relative Path ->
- // Compiled coffee
- // private Date lastCacheUpdate
- // = new Date(0l);
private FilterConfig filterConfig;
private ServletContext servletContext;
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
this.servletContext = this.filterConfig.getServletContext();
- cache = new HashMap<String, CompiledCoffee>();
+ compiler = new CachingCoffeeCompiler();
coffeeFiles = filterConfig.getInitParameter("coffeeFiles");
concatenateRoot = filterConfig.getInitParameter("concatenateRoot");
concatenateName = filterConfig.getInitParameter("concatenateName");
@@ -97,53 +74,60 @@ public void destroy() {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
- if (!isEnabled()) {
- chain.doFilter(request, response);
- return;
- }
HttpServletRequest httpReq = (HttpServletRequest) request;
String requestURI = httpReq.getRequestURI();
ServletContext servletContext = filterConfig.getServletContext();
String contextPath = servletContext.getContextPath();
+ if (!isEnabled() || !requestURI.endsWith(".js")) {
+ chain.doFilter(request, response);
+ return;
+ }
if (requestURI.startsWith(contextPath)) {
requestURI = requestURI.substring(contextPath.length());
}
- String coffeeRequestURI = requestURI;
- if (requestURI.endsWith(".js")) {
- coffeeRequestURI = requestURI.substring(0, requestURI.length() - 3)
- + ".coffee";
- }
- InputStream stream = servletContext.getResourceAsStream("/WEB-INF"
- + coffeeRequestURI);
- if (concatenationEnabled && requestURI.equals(concatenateName)) {
+ String coffeeRequestURI = requestURI.substring(0, requestURI.length() - 3) + ".coffee";
+ if (concatenationEnabled && requestURI.equals(concatenateName) && concatRootExists(servletContext)) {
response.setContentType("text/javascript");
- String coffee = concatenateResourcesAsString(
- rootCoffeePaths(), allCoffeePaths());
- String compiledCoffee = compileCoffeescript(coffeeRequestURI, coffee);
+ Supplier<String> coffeeSupplier = concatenateResourcesSupplier();
+ String compiledCoffee = compiler.compile(requestURI, coffeeSupplier);
response.getOutputStream().print(compiledCoffee);
- cache.put(requestURI,
- new CompiledCoffee(new Date(), compiledCoffee));
return;
}
- if (stream == null) {
+ String resourcePath = "/WEB-INF" + coffeeRequestURI;
+ if (!antPathMatcher.match(coffeeFiles, resourcePath)) {
chain.doFilter(request, response);
return;
}
- response.setContentType("text/javascript");
- // Check the cache.
- CompiledCoffee cc = cache.get(requestURI);
- if (cc != null
- // && cc.sourceLastModified.equals(file.lastModified())
- ) {
- response.getOutputStream().print(cc.output);
+
+ URL resourceUrl = servletContext.getResource(resourcePath);
+ if (resourceUrl == null) {
+ chain.doFilter(request, response);
return;
}
- // Compile the coffee and return.
- String coffee = CharStreams.toString(new InputStreamReader(stream));
- String compiledCoffee = compileCoffeescript(coffeeRequestURI, coffee);
- response.getOutputStream().print(compiledCoffee);
- cache.put(requestURI, new CompiledCoffee(new Date(), compiledCoffee));
- // Render a nice error page?
+
+ response.setContentType("text/javascript");
+ response.getOutputStream().print(compiler.compile(requestURI, urlAsStringSupplier(resourceUrl)));
+
+ }
+
+ private Supplier<String> urlAsStringSupplier(final URL resourceUrl) {
+ return new Supplier<String>() {
+ public String get() {
+ try {
+ InputStream in = resourceUrl.openStream();
+ Closeables.closeQuietly(in);
+ String result = CharStreams.toString(new InputStreamReader(in));
+ return result;
+ } catch (IOException e) {
+ throw new RuntimeException();
+ }
+ }
+ };
+ }
+
+ private boolean concatRootExists(ServletContext servletContext)
+ throws MalformedURLException {
+ return null != servletContext.getResource(concatenateRoot);
}
/*
@@ -154,16 +138,6 @@ protected boolean isEnabled() {
return true;
}
- private String compileCoffeescript(String requestURI, String coffee) {
- try {
- return getCompiler().compile(coffee);
- } catch (JCoffeeScriptCompileException e) {
- e.printStackTrace();
- throw new CompilationException(requestURI, coffee,
- e.getMessage(), getLineNumber(e), -1, -1);
- }
- }
-
private Iterable<String> rootCoffeePaths()
throws IOException {
ServletContextPatternResolver resolver = new ServletContextPatternResolver(
@@ -176,20 +150,27 @@ private String compileCoffeescript(String requestURI, String coffee) {
throws IOException {
ServletContextPatternResolver resolver = new ServletContextPatternResolver(
servletContext);
- return resolver.getResourcePaths(concatenateRoot);
+ return resolver.getResourcePaths(coffeeFiles);
}
- private String concatenateResourcesAsString(
- Iterable<String> rootResources,
- Iterable<String> includeResources) throws IOException {
- Iterable<VirtualFile> rootFiles = resourcesToFiles(rootResources);
- Iterable<VirtualFile> includeFiles = resourcesToFiles(includeResources);
- return concatenateFilesAsString(rootFiles, includeFiles);
+ private Supplier<String> concatenateResourcesSupplier() throws IOException {
+ return new Supplier<String> (){
+ public String get() {
+ try {
+ Iterable<VirtualFile> rootFiles = resourcesToFiles(rootCoffeePaths());
+ Iterable<VirtualFile> includeFiles = resourcesToFiles(allCoffeePaths());
+ return concatenateFilesAsString(rootFiles, includeFiles);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+
}
private String concatenateFilesAsString(Iterable<VirtualFile> rootFiles,
Iterable<VirtualFile> includeFiles) throws IOException {
- return new CoffeescriptConcatenate().concatenate(rootFiles, includeFiles);
+ return new CoffeescriptConcatenate().concatenate(rootFiles, includeFiles);
}
private Iterable<VirtualFile> resourcesToFiles(Iterable<String> rootResources) {
@@ -210,20 +191,5 @@ public VirtualFile apply(String path) {
return rootFiles;
}
- /**
- * @return the line number that the exception happened on, or 0 if not found
- * in the message.
- */
- public static int getLineNumber(JCoffeeScriptCompileException e) {
- Matcher m = LINE_NUMBER.matcher(e.getMessage());
- if (m.find()) {
- return Integer.parseInt(m.group(1));
- }
- return 0;
- }
-
- public static JCoffeeScriptCompiler getCompiler() {
- return compiler.get();
- }
}
@@ -0,0 +1,90 @@
+package com.cadrlife.coffee.compile;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.cadrlife.coffee.jcoffeescript.JCoffeeScriptCompileException;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+
+/*
+ * Caches by both source code and filename. Will recompile when the source code has changed.
+ * Stores up to 100 files by default. This can be changed by passing a custom CacheBuilder to the constructor.
+ */
+public class CachingCoffeeCompiler {
+ public static class CacheOptions {
+ public int maxSize = 100;
+ public int expirationTime = 10;
+ public TimeUnit expirationTimeUnit = TimeUnit.MINUTES;
+ }
+ private final Cache<CompilationCacheFilenameKey, String> cache;
+
+ // Regex to get the line number of the failure.
+ private static final Pattern LINE_NUMBER = Pattern.compile("line ([0-9]+)");
+ private ThreadSafeCoffeeScriptCompiler compiler;
+
+ public CachingCoffeeCompiler() {
+ this(new CacheOptions());
+ }
+
+ public CachingCoffeeCompiler(CacheOptions cacheOptions) {
+ this(cacheOptions, new ThreadSafeCoffeeScriptCompiler());
+ }
+
+ CachingCoffeeCompiler(CacheOptions cacheOptions, ThreadSafeCoffeeScriptCompiler compiler) {
+ this.compiler = compiler;
+ this.cache = CacheBuilder.newBuilder()
+ .maximumSize(cacheOptions.maxSize)
+ .expireAfterWrite(cacheOptions.expirationTime, cacheOptions.expirationTimeUnit)
+ .build(new CoffeeCacheLoader());
+ }
+
+
+ /*
+ * This call will return the cached version if it exists, otherwise will
+ * block until compiler finishes. Will only invoke the supplier as needed.OsOs
+ */
+ public String compile(String requestURI, Supplier<String> stringSupplier) {
+ CompilationCacheFilenameKey key = new CompilationCacheFilenameKey();
+ key.filename = requestURI;
+ key.sourceCodeSupplier = stringSupplier;
+ return cache.getUnchecked(key);
+ }
+
+ public String compile(String requestURI, String coffee) {
+ return compile(requestURI, Suppliers.ofInstance(coffee));
+ }
+
+ /**
+ * @return the line number that the exception happened on, or 0 if not found
+ * in the message.
+ */
+ public static int getLineNumber(JCoffeeScriptCompileException e) {
+ Matcher m = LINE_NUMBER.matcher(e.getMessage());
+ if (m.find()) {
+ return Integer.parseInt(m.group(1));
+ }
+ return 0;
+ }
+
+ private final class CoffeeCacheLoader extends CacheLoader<CompilationCacheFilenameKey, String> {
+ @Override
+ public String load(CompilationCacheFilenameKey request) throws Exception {
+ String sourceCode = request.sourceCodeSupplier.get();
+ try {
+ System.out.println("cmp " + request.filename);
+ return compiler.compile(sourceCode);
+ } catch (JCoffeeScriptCompileException e) {
+ e.printStackTrace();
+ throw new CompilationException(request.filename,
+ sourceCode, e.getMessage(), getLineNumber(e),
+ -1, -1);
+ }
+ }
+ }
+
+}
Oops, something went wrong.

0 comments on commit 72b849d

Please sign in to comment.