├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── sass-java-gems ├── Gemfile ├── Gemfile.lock ├── pom.xml └── src │ ├── main │ └── resources │ │ └── .gitkeep │ └── test │ └── java │ └── com │ └── darrinholst │ └── sass_java │ └── GemsIT.java ├── sass-java-maven ├── README.md ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── darrinholst │ └── sass_java │ └── SassJavaMojo.java └── sass-java ├── pom.xml └── src ├── main └── java │ └── com │ └── darrinholst │ └── sass_java │ ├── Clock.java │ ├── Compiler.java │ ├── Config.java │ └── SassCompilingFilter.java └── test ├── java └── com │ └── darrinholst │ └── sass_java │ ├── FakeClock.java │ └── SassCompilingFilterTest.java └── webapp ├── WEB-INF ├── lib │ └── slf4j-simple-1.7.7.jar ├── sass │ ├── application.scss │ ├── config.rb │ └── reset.scss └── web.xml └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | *.ipr 4 | *.iws 5 | .sass-cache 6 | *.idea 7 | .bundle 8 | *.css 9 | *.css.map 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Darrin Holst 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | Compile sass to css on-the-fly via a j2ee servlet filter 3 | 4 | ## Usage 5 | 6 | Add the dependency to `pom.xml` 7 | 8 | ```xml 9 | 10 | com.darrinholst 11 | sass-java 12 | 3.4.4 13 | 14 | ``` 15 | 16 | Add the filter to `web.xml` 17 | 18 | ```xml 19 | 20 | SassCompiler 21 | com.darrinholst.sass_java.SassCompilingFilter 22 | 23 | 24 | 25 | SassCompiler 26 | *.css 27 | 28 | ``` 29 | 30 | Create a `WEB-INF/sass/config.rb` 31 | 32 | ```ruby 33 | css_dir = "../../stylesheets" 34 | sass_dir = "." 35 | ``` 36 | 37 | Put your sass templates in `WEB-INF/sass` and each request for a css 38 | file will regenerate as needed. 39 | 40 | ## Versioning 41 | The first 3 nodes of the version will match the version of sass that is being used. Current versions of sass and compass can be found in the [Gemfile](https://github.com/darrinholst/sass-java/blob/master/sass-java-gems/Gemfile). 42 | 43 | ## Configuring 44 | Configuration is done through a combination of filter init parameters and the `config.rb` file. The following filter init parameters are available to control the execution of the filter: 45 | 46 | * **configLocation** - the location of the config.rb (default WEB-INF/sass/config.rb) 47 | * **onlyRunWhenKey** - the system property or environment variable to check to see if sass compilation should run, use this to turn sass generation off in production 48 | * **onlyRunWhenValue** - the corresponding value to check to see if sass compilation should run 49 | 50 | A common practice is to turn sass generation off in production and precompile in your build process. An example of how to do this based off a system property is: 51 | 52 | ```xml 53 | 54 | SassCompiler 55 | com.darrinholst.sass_java.SassCompilingFilter 56 | 57 | onlyRunWhenKey 58 | RUNTIME_ENVIRONMENT 59 | 60 | 61 | onlyRunWhenValue 62 | local 63 | 64 | 65 | ``` 66 | 67 | With this configuration the filter will check a system property or environment variable called RUNTIME_ENVIRONMENT and only run the sass compilation if that value is equal to local 68 | 69 | See the [compass config documentation](http://compass-style.org/help/documentation/configuration-reference/) to find out about all the wonderful things you can put in `config.rb`. For those config options that reference a file or directory, the working directory that compass will be executed in is the directory that contains `config.rb`. 70 | 71 | ## Precompiling 72 | Use the [maven plugin](https://github.com/darrinholst/sass-java/blob/master/sass-java-maven/README.md) 73 | 74 | ## Development 75 | The magic behind how this works comes from packaging up the sass gems into a jar that the filter can then use via jruby. The process to jar up the gems is described [here](http://blog.nicksieger.com/articles/2009/01/10/jruby-1-1-6-gems-in-a-jar/). That process is mavenized [here](https://github.com/darrinholst/sass-java/blob/master/sass-java-gems/pom.xml#L34). So to change sass or compass versions All Ya Gotta Do™ is update the [Gemfile](https://github.com/darrinholst/sass-java/blob/master/sass-java-gems/Gemfile) and `mvn clean install`. 76 | 77 | ## Releasing 78 | First time? [Read this](http://central.sonatype.org/pages/working-with-pgp-signatures.html) 79 | `mvn release:prepare release:perform` 80 | That worked? [Read this](http://central.sonatype.org/pages/releasing-the-deployment.html) 81 | 82 | ## Try it out 83 | 1. git clone https://github.com/darrinholst/sass-java.git 84 | 2. cd sass-java 85 | 3. mvn install 86 | 4. mvn jetty:run -pl sass-java 87 | 5. open http://localhost:8080 88 | 6. change src/test/sample-webapp/WEB-INF/sass/application.scss 89 | 7. refresh 90 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.darrinholst 6 | sass-java-parent 7 | 3.4.20.1-SNAPSHOT 8 | pom 9 | 10 | 11 | org.sonatype.oss 12 | oss-parent 13 | 7 14 | 15 | 16 | 17 | 18 | 19 | org.jruby 20 | jruby-complete 21 | 1.7.15 22 | 23 | 24 | junit 25 | junit 26 | 4.8.2 27 | 28 | 29 | 30 | 31 | 32 | sass-java-gems 33 | sass-java 34 | sass-java-maven 35 | 36 | 37 | Sass for Java 38 | 39 | 40 | 41 | MIT License 42 | http://www.opensource.org/licenses/mit-license.php 43 | 44 | 45 | 46 | 47 | https://github.com/darrinholst/sass-java 48 | scm:git:git@github.com:darrinholst/sass-java.git 49 | 50 | 51 | -------------------------------------------------------------------------------- /sass-java-gems/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'compass', '1.0.1' 4 | gem 'sass', '3.4.20' 5 | gem 'ffi', '1.9.10' 6 | -------------------------------------------------------------------------------- /sass-java-gems/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | chunky_png (1.3.5) 5 | compass (1.0.1) 6 | chunky_png (~> 1.2) 7 | compass-core (~> 1.0.1) 8 | compass-import-once (~> 1.0.5) 9 | rb-fsevent (>= 0.9.3) 10 | rb-inotify (>= 0.9) 11 | sass (>= 3.3.13, < 3.5) 12 | compass-core (1.0.3) 13 | multi_json (~> 1.0) 14 | sass (>= 3.3.0, < 3.5) 15 | compass-import-once (1.0.5) 16 | sass (>= 3.2, < 3.5) 17 | ffi (1.9.10-java) 18 | multi_json (1.11.2) 19 | rb-fsevent (0.9.6) 20 | rb-inotify (0.9.5) 21 | ffi (>= 0.5.0) 22 | sass (3.4.20) 23 | 24 | PLATFORMS 25 | java 26 | 27 | DEPENDENCIES 28 | compass (= 1.0.1) 29 | ffi (= 1.9.10) 30 | sass (= 3.4.20) 31 | 32 | BUNDLED WITH 33 | 1.11.1 34 | -------------------------------------------------------------------------------- /sass-java-gems/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.darrinholst 7 | sass-java-parent 8 | 3.4.20.1-SNAPSHOT 9 | 10 | 11 | sass-java-gems 12 | 3.4.20.1-SNAPSHOT 13 | jar 14 | 15 | 16 | 17 | org.jruby 18 | jruby-complete 19 | runtime 20 | 21 | 22 | junit 23 | junit 24 | test 25 | 26 | 27 | 28 | 29 | 30 | 31 | org.codehaus.mojo 32 | exec-maven-plugin 33 | 1.3.2 34 | 35 | 36 | install-bundler 37 | prepare-package 38 | 39 | exec 40 | 41 | 42 | java 43 | 44 | -classpath 45 | org.jruby:jruby-complete 46 | org.jruby.Main 47 | -S 48 | gem 49 | install 50 | -i 51 | ${project.build.directory}/bundler 52 | bundler 53 | 54 | 55 | 56 | 57 | bundle-install 58 | prepare-package 59 | 60 | exec 61 | 62 | 63 | 64 | ${project.build.directory}/bundler 65 | 66 | java 67 | 68 | -classpath 69 | org.jruby:jruby-complete 70 | org.jruby.Main 71 | -S 72 | ${project.build.directory}/bundler/bin/bundle 73 | install 74 | --gemfile 75 | ${basedir}/Gemfile 76 | --path 77 | ${project.build.directory}/bundle 78 | 79 | 80 | 81 | 82 | jar-it-up 83 | package 84 | 85 | exec 86 | 87 | 88 | jar 89 | 90 | cf 91 | ${project.build.directory}/${project.name}-${project.version}.jar 92 | -C 93 | ${project.build.directory}/bundle/jruby/1.9 94 | . 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-surefire-plugin 103 | 2.17 104 | 105 | 106 | integration-test 107 | 108 | test 109 | 110 | 111 | 112 | ${project.build.directory}/${project.name}-${project.version}.jar 113 | 114 | 115 | **/*IT.java 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /sass-java-gems/src/main/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrinholst/sass-java/9e440151a5a6f38b51f504e2ffad0d10328f87f5/sass-java-gems/src/main/resources/.gitkeep -------------------------------------------------------------------------------- /sass-java-gems/src/test/java/com/darrinholst/sass_java/GemsIT.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import org.jruby.embed.ScriptingContainer; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | 8 | public class GemsIT { 9 | @Test 10 | public void correctSassIsPackaged() { 11 | assertEquals("3.4.20", new ScriptingContainer().runScriptlet("require 'sass';Sass.version[:number]")); 12 | } 13 | 14 | @Test 15 | public void correctCompassIsPackaged() { 16 | assertEquals("1.0.1", new ScriptingContainer().runScriptlet("require 'compass';Compass::VERSION")); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /sass-java-maven/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | Compile sass during compile phase of a maven build. 3 | 4 | ## Usage 5 | 6 | Add the dependency to `pom.xml` 7 | 8 | ```xml 9 | 10 | com.darrinholst 11 | sass-java-maven-plugin 12 | 3.4.4.3 13 | 14 | 15 | 16 | compile 17 | 18 | 19 | 20 | 21 | ``` 22 | 23 | Alternatively you can run the plugin from the command-line like this: 24 | ``` 25 | mvn com.darrinholst:sass-java-maven-plugin:compile 26 | ``` 27 | 28 | ## Advanced configuration 29 | 30 | To make running the plugin even more simple it is possible to add `com.darrinholst` to 31 | the list of plugin-groups in your `[userhome]/.m2/settings.xml`. 32 | 33 | ```xml 34 | 35 | com.darrinholst 36 | 37 | ``` 38 | 39 | After that you'll be able to run the command as a short-hand. Even if the plugin has not 40 | been configured for your project, even when a pom.xml isn't even available. 41 | 42 | ``` 43 | mvn sass-java:compile 44 | ``` 45 | 46 | ### Config file location 47 | 48 | The default for the location of the `config.rb` file is set to the same location as the 49 | sass-java filter, being `${project.basedir}/src/main/webapp/WEB-INF/sass/config.rb`. You 50 | can configure this location in the `pom.xml` using the configurations element. 51 | 52 | ```xml 53 | 54 | com.darrinholst 55 | sass-java-maven-plugin 56 | 3.4.4.3 57 | 58 | 59 | 60 | ${project.basedir}/src/main/webapp/WEB-INF/sass/config.rb 61 | 62 | 63 | compile 64 | 65 | 66 | 67 | 68 | ``` 69 | 70 | Alternatively you can set the property on commandline as follows: 71 | 72 | ``` 73 | mvn -Dsass-java.configFile="${project.basedir}/src/main/webapp/WEB-INF/sass/config.rb" com.darrinholst:sass-java-maven-plugin:compile 74 | ``` 75 | -------------------------------------------------------------------------------- /sass-java-maven/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.darrinholst 7 | sass-java-parent 8 | 3.4.20.1-SNAPSHOT 9 | 10 | 11 | sass-java-maven-plugin 12 | 3.4.20.1-SNAPSHOT 13 | maven-plugin 14 | 15 | 16 | 17 | com.darrinholst 18 | sass-java 19 | ${project.version} 20 | 21 | 22 | org.apache.maven 23 | maven-plugin-api 24 | 3.2.3 25 | 26 | 27 | org.apache.maven.plugin-tools 28 | maven-plugin-annotations 29 | 3.3 30 | provided 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-plugin-plugin 39 | 3.3 40 | 41 | 42 | true 43 | 44 | 45 | 46 | mojo-descriptor 47 | 48 | descriptor 49 | 50 | 51 | 52 | 53 | help-goal 54 | 55 | helpmojo 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /sass-java-maven/src/main/java/com/darrinholst/sass_java/SassJavaMojo.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import org.apache.maven.plugin.AbstractMojo; 4 | import org.apache.maven.plugin.MojoExecutionException; 5 | import org.apache.maven.plugin.MojoFailureException; 6 | import org.apache.maven.plugins.annotations.LifecyclePhase; 7 | import org.apache.maven.plugins.annotations.Mojo; 8 | import org.apache.maven.plugins.annotations.Parameter; 9 | 10 | import java.io.File; 11 | 12 | @Mojo(name = "compile", defaultPhase = LifecyclePhase.COMPILE, requiresDirectInvocation = false) 13 | public class SassJavaMojo extends AbstractMojo { 14 | private Compiler compiler = new Compiler(); 15 | 16 | @Parameter(defaultValue = "${project.basedir}", readonly = true) 17 | private File basedir; 18 | 19 | @Parameter(defaultValue = "${project.basedir}/src/main/webapp/WEB-INF/sass/config.rb", property = "sass-java.configFile") 20 | private File config; 21 | 22 | public void execute() throws MojoExecutionException, MojoFailureException { 23 | if (config.exists()) { 24 | compiler.setConfigLocation(config); 25 | compiler.compile(); 26 | } else { 27 | throw new MojoExecutionException("Configuration does not exist at " + config.getAbsolutePath()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sass-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.darrinholst 7 | sass-java-parent 8 | 3.4.20.1-SNAPSHOT 9 | 10 | 11 | sass-java 12 | 3.4.20.1-SNAPSHOT 13 | jar 14 | 15 | 16 | 17 | org.jruby 18 | jruby-complete 19 | 20 | 21 | com.darrinholst 22 | sass-java-gems 23 | 3.4.20.1-SNAPSHOT 24 | 25 | 26 | org.slf4j 27 | slf4j-api 28 | 1.6.1 29 | 30 | 31 | javax.servlet 32 | servlet-api 33 | 2.5 34 | provided 35 | 36 | 37 | org.slf4j 38 | slf4j-simple 39 | 1.7.7 40 | test 41 | 42 | 43 | junit 44 | junit 45 | test 46 | 47 | 48 | commons-io 49 | commons-io 50 | 1.3.2 51 | test 52 | 53 | 54 | org.mockito 55 | mockito-core 56 | 1.8.5 57 | test 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-surefire-plugin 66 | 2.17 67 | 68 | 69 | ${project.parent.relativePath}/sass-java-gems/target/sass-java-gems-${project.version}.jar 70 | 71 | 72 | 73 | 74 | org.eclipse.jetty 75 | jetty-maven-plugin 76 | 9.2.3.v20140905 77 | 78 | src/test/webapp 79 | foo 80 | 9999 81 | 82 | 83 | RUNTIME_ENVIRONMENT 84 | local 85 | 86 | 87 | 88 | 89 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /sass-java/src/main/java/com/darrinholst/sass_java/Clock.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import java.util.Date; 4 | 5 | public class Clock { 6 | private static Clock delegate = new Clock(); 7 | 8 | public static void setDelegate(Clock clock) { 9 | delegate = clock; 10 | } 11 | 12 | public static Date now() { 13 | return delegate.getCurrentTime(); 14 | } 15 | 16 | protected Date getCurrentTime() { 17 | return new Date(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sass-java/src/main/java/com/darrinholst/sass_java/Compiler.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import org.jruby.embed.ScriptingContainer; 4 | 5 | import java.io.File; 6 | import java.io.PrintWriter; 7 | import java.io.StringWriter; 8 | 9 | public class Compiler { 10 | private boolean initialized; 11 | private File configLocation; 12 | 13 | public void compile() { 14 | initialize(); 15 | new ScriptingContainer().runScriptlet(buildCompileScript()); 16 | } 17 | 18 | private void initialize() { 19 | if (!initialized) { 20 | new ScriptingContainer().runScriptlet(buildInitializationScript()); 21 | initialized = true; 22 | } 23 | } 24 | 25 | private String buildCompileScript() { 26 | StringWriter raw = new StringWriter(); 27 | PrintWriter script = new PrintWriter(raw); 28 | 29 | script.println("Dir.chdir(File.dirname('" + getConfigLocation() + "')) do "); 30 | script.println(" compiler = Compass.sass_compiler "); 31 | script.println(" compiler.logger = Compass::NullLogger.new "); 32 | script.println(" compiler.clean! "); 33 | script.println(" compiler.compile! "); 34 | script.println("end "); 35 | script.flush(); 36 | 37 | return raw.toString(); 38 | } 39 | 40 | private String buildInitializationScript() { 41 | StringWriter raw = new StringWriter(); 42 | PrintWriter script = new PrintWriter(raw); 43 | 44 | script.println("require 'compass' "); 45 | script.println("require 'compass/sass_compiler' "); 46 | script.println("Compass.add_project_configuration '" + getConfigLocation() + "' "); 47 | script.println("Compass.configure_sass_plugin! "); 48 | script.flush(); 49 | 50 | return raw.toString(); 51 | } 52 | 53 | private String getConfigLocation() { 54 | return replaceSlashes(configLocation.getAbsolutePath()); 55 | } 56 | 57 | private String replaceSlashes(String path) { 58 | return path.replaceAll("\\\\", "/"); 59 | } 60 | 61 | public void setConfigLocation(File configLocation) { 62 | this.configLocation = configLocation; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sass-java/src/main/java/com/darrinholst/sass_java/Config.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import javax.servlet.FilterConfig; 4 | import java.io.File; 5 | 6 | public class Config { 7 | private FilterConfig filterConfig; 8 | 9 | public Config(FilterConfig filterConfig) { 10 | this.filterConfig = filterConfig; 11 | } 12 | 13 | public File getRootPath() { 14 | return new File(filterConfig.getServletContext().getRealPath("/")); 15 | } 16 | 17 | public String getString(String parameterName) { 18 | return getString(parameterName, null); 19 | } 20 | 21 | public String getString(String parameterName, String defaultValue) { 22 | String value = filterConfig.getInitParameter(parameterName); 23 | 24 | if (value == null) { 25 | value = defaultValue; 26 | } 27 | 28 | return value; 29 | } 30 | 31 | public boolean getBoolean(String parameterName, boolean defaultValue) { 32 | String value = filterConfig.getInitParameter(parameterName); 33 | 34 | if (value == null) { 35 | return defaultValue; 36 | } else { 37 | return Boolean.parseBoolean(value); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sass-java/src/main/java/com/darrinholst/sass_java/SassCompilingFilter.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.servlet.*; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.lang.*; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | public class SassCompilingFilter implements Filter { 13 | private static final Logger LOG = LoggerFactory.getLogger(SassCompilingFilter.class); 14 | private static final int DWELL = 1000; 15 | protected static final String RETHROW_EXCEPTIONS_PARAM = "rethrowExceptions"; 16 | protected static final String ONLY_RUN_KEY_PARAM = "onlyRunWhenKey"; 17 | protected static final String ONLY_RUN_VALUE_PARAM = "onlyRunWhenValue"; 18 | protected static final String CONFIG_LOCATION_PARAM = "configLocation"; 19 | protected static final String DEFAULT_CONFIG_LOCATION = "WEB-INF" + File.separator + "sass" + File.separator + "config.rb"; 20 | 21 | private long lastRun; 22 | private String onlyRunWhenKey; 23 | private String onlyRunWhenValue; 24 | private boolean rethrowExceptions; 25 | private Compiler compiler = new Compiler(); 26 | private AtomicBoolean compiling = new AtomicBoolean(false); 27 | 28 | public void init(FilterConfig filterConfig) throws ServletException { 29 | Config config = new Config(filterConfig); 30 | onlyRunWhenKey = config.getString(ONLY_RUN_KEY_PARAM); 31 | onlyRunWhenValue = config.getString(ONLY_RUN_VALUE_PARAM); 32 | rethrowExceptions = config.getBoolean(RETHROW_EXCEPTIONS_PARAM, false); 33 | 34 | compiler.setConfigLocation(new File( 35 | config.getRootPath(), 36 | config.getString(CONFIG_LOCATION_PARAM, DEFAULT_CONFIG_LOCATION) 37 | )); 38 | 39 | if (environmentAllowsRunning()) compiler.compile(); 40 | 41 | } 42 | 43 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 44 | if (shouldRun()) { 45 | run(); 46 | } 47 | 48 | while (compiling.get()) waitABit(); 49 | 50 | filterChain.doFilter(servletRequest, servletResponse); 51 | } 52 | 53 | private void waitABit() { 54 | try { 55 | Thread.sleep(100L); 56 | } catch (InterruptedException e) { 57 | 58 | } 59 | } 60 | 61 | private void run() { 62 | LOG.debug("compiling sass"); 63 | 64 | try { 65 | compiling.set(true); 66 | compiler.compile(); 67 | } catch (Exception e) { 68 | LOG.warn("exception thrown while compiling sass", e); 69 | 70 | if (rethrowExceptions) { 71 | throw new RuntimeException(e); 72 | } 73 | } finally { 74 | compiling.set(false); 75 | } 76 | } 77 | 78 | private boolean shouldRun() { 79 | return !compiling.get() && environmentAllowsRunning() && timeToRun(); 80 | } 81 | 82 | private boolean environmentAllowsRunning() { 83 | if (onlyRunWhenKey != null) { 84 | String value = System.getProperty(onlyRunWhenKey, System.getenv(onlyRunWhenKey)); 85 | return onlyRunWhenValue.equalsIgnoreCase(value); 86 | } 87 | return true; 88 | } 89 | 90 | private synchronized boolean timeToRun() { 91 | long now = Clock.now().getTime(); 92 | 93 | if (now - lastRun >= DWELL) { 94 | lastRun = Clock.now().getTime(); 95 | return true; 96 | } else { 97 | return false; 98 | } 99 | } 100 | 101 | public void destroy() { 102 | } 103 | 104 | public void setCompiler(Compiler compiler) { 105 | this.compiler = compiler; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /sass-java/src/test/java/com/darrinholst/sass_java/FakeClock.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import java.util.Date; 4 | 5 | public class FakeClock extends Clock { 6 | protected Date stopped = new Date(); 7 | 8 | @Override 9 | protected Date getCurrentTime() { 10 | return stopped; 11 | } 12 | 13 | public void incrementSeconds(int seconds) { 14 | stopped = new Date(stopped.getTime() + (seconds * 1000)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sass-java/src/test/java/com/darrinholst/sass_java/SassCompilingFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.darrinholst.sass_java; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.TemporaryFolder; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.ArgumentCaptor; 11 | import org.mockito.Mock; 12 | import org.mockito.runners.MockitoJUnitRunner; 13 | 14 | import javax.servlet.*; 15 | import java.io.File; 16 | import java.io.FileOutputStream; 17 | import java.io.IOException; 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.List; 21 | import java.util.concurrent.CountDownLatch; 22 | import java.util.concurrent.atomic.AtomicBoolean; 23 | 24 | import static com.darrinholst.sass_java.SassCompilingFilter.*; 25 | import static java.util.Arrays.asList; 26 | import static junit.framework.Assert.*; 27 | import static org.mockito.Matchers.eq; 28 | import static org.mockito.Mockito.*; 29 | 30 | @RunWith(MockitoJUnitRunner.class) 31 | public class SassCompilingFilterTest { 32 | private static final String ONLY_RUN_KEY = "unit_test_environment"; 33 | private static final String CSS_LOCATION = "css"; 34 | private static final String SASS_LOCATION = "WEB-INF/sass"; 35 | 36 | @Rule 37 | public TemporaryFolder temporaryFolder = new TemporaryFolder(); 38 | 39 | @Mock 40 | ServletRequest servletRequest; 41 | @Mock 42 | ServletResponse servletResponse; 43 | @Mock 44 | ServletContext servletContext; 45 | @Mock 46 | FilterConfig filterConfig; 47 | @Mock 48 | FilterChain filterChain; 49 | 50 | private String webAppRoot; 51 | private FakeClock clock; 52 | private SassCompilingFilter filter; 53 | 54 | @Before 55 | public void setup() throws Exception { 56 | webAppRoot = temporaryFolder.newFolder("").getAbsolutePath(); 57 | Clock.setDelegate(clock = new FakeClock()); 58 | when(filterConfig.getServletContext()).thenReturn(servletContext); 59 | when(filterConfig.getInitParameter(RETHROW_EXCEPTIONS_PARAM)).thenReturn("true"); 60 | when(servletContext.getRealPath("/")).thenReturn(webAppRoot); 61 | 62 | filter = new SassCompilingFilter(); 63 | } 64 | 65 | @After 66 | public void teardown() throws Exception { 67 | System.clearProperty(ONLY_RUN_KEY); 68 | } 69 | 70 | @Test 71 | public void initShouldCompileFiles() throws ServletException { 72 | com.darrinholst.sass_java.Compiler compiler = mock(Compiler.class); 73 | 74 | filter.setCompiler(compiler); 75 | 76 | initFilter(); 77 | 78 | verify(compiler).compile(); 79 | } 80 | 81 | @Test 82 | public void compilerShouldNotBeInvokedIfItIsAlreadyCompiling() throws Exception { 83 | StubCompiler compiler = new StubCompiler(1L, 2000L, 1L); 84 | filter.setCompiler(compiler); 85 | 86 | setupDefaultDirectoriesAndConfigFile(); 87 | initFilter(); 88 | 89 | Thread thread = processRequestOnAnotherThread(servletRequest); 90 | clock.incrementSeconds(4); 91 | runFilter(servletRequest); 92 | 93 | thread.join(); 94 | 95 | assertEquals(2, compiler.getNumberOfCompiles()); 96 | } 97 | 98 | @Test 99 | public void requestsShouldBlockUntilTheCompilingHasCompleted() throws Exception { 100 | ArgumentCaptor captor = ArgumentCaptor.forClass(ServletRequest.class); 101 | filter.setCompiler(new StubCompiler(2000L, 1L)); 102 | final ServletRequest request = mock(ServletRequest.class, "request"); 103 | ServletRequest otherRequest = mock(ServletRequest.class, "otherRequest"); 104 | 105 | setupDefaultDirectoriesAndConfigFile(); 106 | initFilter(); 107 | 108 | Thread thread = processRequestOnAnotherThread(request); 109 | runFilter(otherRequest); 110 | 111 | thread.join(); 112 | 113 | verify(filterChain, times(2)).doFilter(captor.capture(), eq(servletResponse)); 114 | assertEquals(Arrays.asList(request, otherRequest), captor.getAllValues()); 115 | } 116 | 117 | @Test 118 | public void compiles() throws Exception { 119 | setupDefaultDirectoriesAndConfigFile(); 120 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 121 | 122 | initAndRunFilter(); 123 | 124 | assertEquals(asList("foo.css", "foo.css.map"), directoryListing(CSS_LOCATION)); 125 | } 126 | 127 | @Test 128 | public void doesNotRunIfLastRunWasLessThanOneSecondAgo() throws Exception { 129 | setupDefaultDirectoriesAndConfigFile(); 130 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 131 | 132 | initAndRunFilter(); 133 | 134 | assertDirectoryNotEmpty(CSS_LOCATION); 135 | clearDirectory(CSS_LOCATION); 136 | assertDirectoryEmpty(CSS_LOCATION); 137 | 138 | runFilter(); 139 | 140 | assertDirectoryEmpty(CSS_LOCATION); 141 | } 142 | 143 | @Test 144 | public void runsIfLastRunWasGreaterThanOneSecondAgo() throws Exception { 145 | setupDefaultDirectoriesAndConfigFile(); 146 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 147 | 148 | initAndRunFilter(); 149 | 150 | assertDirectoryNotEmpty(CSS_LOCATION); 151 | clearDirectory(CSS_LOCATION); 152 | assertDirectoryEmpty(CSS_LOCATION); 153 | clock.incrementSeconds(1); 154 | 155 | runFilter(); 156 | 157 | assertDirectoryNotEmpty(CSS_LOCATION); 158 | } 159 | 160 | @Test 161 | public void runsOnlyInDevelopmentMode() throws Exception { 162 | setupDefaultDirectoriesAndConfigFile(); 163 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 164 | when(filterConfig.getInitParameter(ONLY_RUN_KEY_PARAM)).thenReturn(ONLY_RUN_KEY); 165 | when(filterConfig.getInitParameter(ONLY_RUN_VALUE_PARAM)).thenReturn("development"); 166 | System.setProperty(ONLY_RUN_KEY, "development"); 167 | 168 | initAndRunFilter(); 169 | 170 | assertDirectoryNotEmpty(CSS_LOCATION); 171 | } 172 | 173 | @Test 174 | public void runsOnlyInDevelopmentModeAndDoesNotCareAboutCase() throws Exception { 175 | setupDefaultDirectoriesAndConfigFile(); 176 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 177 | when(filterConfig.getInitParameter(ONLY_RUN_KEY_PARAM)).thenReturn(ONLY_RUN_KEY); 178 | when(filterConfig.getInitParameter(ONLY_RUN_VALUE_PARAM)).thenReturn("development"); 179 | System.setProperty(ONLY_RUN_KEY, "dEvElOpMeNt"); 180 | 181 | initAndRunFilter(); 182 | 183 | assertDirectoryNotEmpty(CSS_LOCATION); 184 | } 185 | 186 | @Test 187 | public void doesNotRunInUnknownModes() throws Exception { 188 | setupDefaultDirectoriesAndConfigFile(); 189 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 190 | when(filterConfig.getInitParameter(ONLY_RUN_KEY_PARAM)).thenReturn(ONLY_RUN_KEY); 191 | when(filterConfig.getInitParameter(ONLY_RUN_VALUE_PARAM)).thenReturn("development"); 192 | 193 | initAndRunFilter(); 194 | 195 | assertDirectoryEmpty(CSS_LOCATION); 196 | } 197 | 198 | @Test 199 | public void doesNotRunInProductionMode() throws Exception { 200 | setupDefaultDirectoriesAndConfigFile(); 201 | addScssFileTo(fullPathOf(SASS_LOCATION), "foo"); 202 | when(filterConfig.getInitParameter(ONLY_RUN_KEY_PARAM)).thenReturn(ONLY_RUN_KEY); 203 | when(filterConfig.getInitParameter(ONLY_RUN_VALUE_PARAM)).thenReturn("development"); 204 | System.setProperty(ONLY_RUN_KEY, "production"); 205 | 206 | initAndRunFilter(); 207 | 208 | assertDirectoryEmpty(CSS_LOCATION); 209 | } 210 | 211 | @Test 212 | public void importWorks() throws Exception { 213 | setupDefaultDirectoriesAndConfigFile(); 214 | addScssFileTo(fullPathOf(SASS_LOCATION), "_other", "body {color: #000}"); 215 | addScssFileTo(fullPathOf(SASS_LOCATION), "base", "@import 'other';"); 216 | 217 | initAndRunFilter(); 218 | 219 | assertEquals(asList("base.css", "base.css.map"), directoryListing(CSS_LOCATION)); 220 | String expected = "body{color:#000}" + System.getProperty("line.separator") + "/*# sourceMappingURL=base.css.map */"; 221 | assertEquals(expected, contentsOf(fullPathOf(CSS_LOCATION), "base.css").trim()); 222 | } 223 | 224 | @Test 225 | public void multipleThreads() throws Exception { 226 | setupDefaultDirectoriesAndConfigFile(); 227 | initFilter(); 228 | 229 | CountDownLatch latch = new CountDownLatch(2); 230 | 231 | FilterThread thread1 = new FilterThread(latch); 232 | FilterThread thread2 = new FilterThread(latch); 233 | 234 | thread1.start(); 235 | thread2.start(); 236 | 237 | thread1.join(); 238 | thread2.join(); 239 | 240 | assertFalse("exception thrown in 1st thread", thread1.exceptionThrown); 241 | assertFalse("exception thrown in 2nd thread", thread2.exceptionThrown); 242 | } 243 | 244 | private class FilterThread extends Thread { 245 | private CountDownLatch latch; 246 | public boolean exceptionThrown; 247 | 248 | public FilterThread(CountDownLatch countdown) { 249 | this.latch = countdown; 250 | } 251 | 252 | @Override 253 | public void run() { 254 | try { 255 | latch.countDown(); 256 | latch.await(); 257 | runFilter(); 258 | } catch (Exception e) { 259 | exceptionThrown = true; 260 | throw new RuntimeException(e); 261 | } 262 | } 263 | } 264 | 265 | private Thread processRequestOnAnotherThread(final ServletRequest request) throws InterruptedException { 266 | final AtomicBoolean requestProcessing = new AtomicBoolean(false); 267 | Thread thread = new Thread(new Runnable() { 268 | public void run() { 269 | requestProcessing.set(true); 270 | runFilter(request); 271 | } 272 | }); 273 | thread.start(); 274 | 275 | while (!requestProcessing.get()) { 276 | Thread.sleep(10L); 277 | } 278 | 279 | return thread; 280 | } 281 | 282 | private String contentsOf(File directory, String filename) throws Exception { 283 | return FileUtils.readFileToString(new File(directory, filename)); 284 | } 285 | 286 | private void initAndRunFilter(String... parameters) throws ServletException, IOException { 287 | initFilter(parameters); 288 | runFilter(); 289 | } 290 | 291 | private void initFilter(String... parameters) throws ServletException { 292 | for (int i = 0; i < parameters.length; i += 2) { 293 | when(filterConfig.getInitParameter(parameters[i])).thenReturn(parameters[i + 1]); 294 | } 295 | 296 | filter.init(filterConfig); 297 | } 298 | 299 | private void runFilter() { 300 | runFilter(servletRequest); 301 | } 302 | 303 | private void runFilter(ServletRequest request) { 304 | try { 305 | filter.doFilter(request, servletResponse, filterChain); 306 | } catch (Exception e) { 307 | throw new RuntimeException(e); 308 | } 309 | } 310 | 311 | private void addScssFileTo(File directory, String name) throws Exception { 312 | addScssFileTo(directory, name, ""); 313 | } 314 | 315 | private void addScssFileTo(File directory, String name, String content) throws Exception { 316 | File file = new File(directory, name + ".scss"); 317 | assertTrue(file.createNewFile()); 318 | FileOutputStream output = new FileOutputStream(file); 319 | output.write(content.getBytes()); 320 | output.close(); 321 | } 322 | 323 | private void setupDefaultDirectoriesAndConfigFile() throws Exception { 324 | setupDirectories(CSS_LOCATION, SASS_LOCATION); 325 | 326 | File file = new File(fullPathOf(SASS_LOCATION), "config.rb"); 327 | assertTrue(file.createNewFile()); 328 | FileOutputStream output = new FileOutputStream(file); 329 | output.write(("" + 330 | "css_dir = '../../" + CSS_LOCATION + "'\n" + 331 | "sass_dir = '.'\n" + 332 | "line_comments = false\n" + 333 | "sourcemap = true\n" + 334 | "output_style = :compressed\n").getBytes()); 335 | output.close(); 336 | } 337 | 338 | private void setupDirectories(String cssLocation, String sassLocation) { 339 | assertTrue(fullPathOf(cssLocation).mkdirs()); 340 | assertTrue(fullPathOf(sassLocation).mkdirs()); 341 | } 342 | 343 | private File fullPathOf(String directory) { 344 | return new File(webAppRoot, directory); 345 | } 346 | 347 | private void assertDirectoryNotEmpty(String directoryName) { 348 | assertTrue(directoryName + " should not have been empty", directoryListing(directoryName).size() > 0); 349 | } 350 | 351 | private void assertDirectoryEmpty(String directoryName) { 352 | List list = directoryListing(directoryName); 353 | assertTrue("didn't expect " + list.toString() + " to be in " + directoryName, list.size() == 0); 354 | } 355 | 356 | private void clearDirectory(String directoryName) { 357 | List filenames = directoryListing(directoryName); 358 | 359 | for (String filename : filenames) { 360 | File file = new File(fullPathOf(directoryName), filename); 361 | assertTrue("trying to delete " + filename, file.delete()); 362 | } 363 | } 364 | 365 | private List directoryListing(String directoryName) { 366 | return asList(fullPathOf(directoryName).list()); 367 | } 368 | 369 | private class StubCompiler extends Compiler { 370 | private ArrayList compileTimes = new ArrayList(); 371 | private int i; 372 | 373 | private StubCompiler(Long... compileTimes) { 374 | this.compileTimes.addAll(Arrays.asList(compileTimes)); 375 | } 376 | 377 | @Override 378 | public void compile() { 379 | try { 380 | Thread.sleep(compileTimes.get(i++)); 381 | } catch (InterruptedException e) { 382 | } 383 | } 384 | 385 | public int getNumberOfCompiles() { 386 | return i; 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /sass-java/src/test/webapp/WEB-INF/lib/slf4j-simple-1.7.7.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrinholst/sass-java/9e440151a5a6f38b51f504e2ffad0d10328f87f5/sass-java/src/test/webapp/WEB-INF/lib/slf4j-simple-1.7.7.jar -------------------------------------------------------------------------------- /sass-java/src/test/webapp/WEB-INF/sass/application.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/text-shadow"; 2 | 3 | $background-color: #ccc; 4 | 5 | body { 6 | background: $background-color; 7 | padding: 50px; 8 | } 9 | 10 | h1 { 11 | font-size: 50px; 12 | } 13 | 14 | h2 { 15 | font-size: 40px; 16 | padding-top: 20px; 17 | @include single-text-shadow(magenta, 3px, 3px); 18 | } 19 | -------------------------------------------------------------------------------- /sass-java/src/test/webapp/WEB-INF/sass/config.rb: -------------------------------------------------------------------------------- 1 | css_dir = "../../stylesheets" 2 | sass_dir = "." 3 | sourcemap = true 4 | sass_options = {:quiet => true} 5 | 6 | -------------------------------------------------------------------------------- /sass-java/src/test/webapp/WEB-INF/sass/reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /sass-java/src/test/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | index.html 7 | 8 | 9 | 10 | SassCompiler 11 | com.darrinholst.sass_java.SassCompilingFilter 12 | 13 | onlyRunWhenKey 14 | RUNTIME_ENVIRONMENT 15 | 16 | 17 | onlyRunWhenValue 18 | local 19 | 20 | 21 | configLocation 22 | WEB-INF/sass/config.rb 23 | 24 | 25 | 26 | 27 | SassCompiler 28 | *.css 29 | 30 | -------------------------------------------------------------------------------- /sass-java/src/test/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | SASS 4 | 5 | 6 | 7 | 8 |

ZOMG...SASS IN JAVA!

9 |

Now with Compass!

10 | 11 | 12 | --------------------------------------------------------------------------------