Preprocessing CSS in Grails

Print This Post Print This Post

While generally a static resource, CSS files sometimes require custom tuning. Whether it be to support different browser “standards” (*cough* Internet Explorer *cough*), static resources, or even color themes, it often helps to have the logic build right into the CSS files themselves.

The Stylesheet

The idea is rather straight forward in that we just turn the CSS file into a GSP page in terms of functionality. It still retains the .css extension but gets access to GSP tags, embedded logic and other goodness. I’ll start with a small example that includes a custom tag explained here.

1
2
3
4
5
.some_div {
	padding: 0px;
	margin: 0px;
	background: #D8DCE0 url(${wthr.staticResource(dir:"/img/", file:"gradient-bg.png")}) repeat-x;
}

When processed throught the controller (see below), the output will result as:

1
2
3
4
5
.some_div {
	padding: 0px;
	margin: 0px;
	background: #D8DCE0 url(http://www.mysite.com/static/img/gradient-bg.png) repeat-x;
}

The Controller

This is backed by a single controller. No URL mapping should be required, but you may need to adapt to your applications design. In the version I have in my Grails application, my controller includes some browser cache control functionality, but for this example I will leave that out. The full controller will be displayed at the bottom of this post.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.text.SimpleDateFormat
import org.apache.http.impl.cookie.DateUtils
 
class ResourceController {
 
    def groovyPagesTemplateEngine 
 
    def css = {
        def file = params.id
 
 
        def cssPath = servletContext.getRealPath("css/${file}.css")
 
        def resourceFile = new File(cssPath)
 
        if (!resourceFile.exists()) {
            // 404: Not Found
            render(text:"File Not Found",status:404)
            return
    	}
 
        def buffer = null
        try {
            buffer = processTemplate(resourceFile, params)
        } catch (Exception ex) {
            log.error "Failed to process template", ex
        }
 
        if (buffer != null) {
            render(text:buffer, contentType:"text/css")
    	} else {
            // 500: Internal Server Error
            render(text:"Failed to process template", status:500)
    	}
 
    }
 
    def processTemplate(resourceFile, model)
    {	
        def buffer = resourceFile.getText()
    	def template = groovyPagesTemplateEngine.createTemplate(buffer,"${resourceFile.getPath()}")
    	def writer = new StringWriter()
    	template.make(model).writeTo(writer)
    	return writer.toString()
    }
}

The steps in the example are:

  1. Get the actual path of the css file from the ServletContent (line 12). In this example it would be under myapp.war/css/${file}.css
  2. Create a File instance and verify that the file exists. If it doesn’t, render a 404 error. (lines 14-19)
  3. Process the template through the Groovy Pages Template Engine service. (lines 22-27 & lines 38-45)
  4. Render the resulting CSS to the browser. If the content happens to be null at this point, instead return a 500 error. (lines 29-34)

Including on Your Page

Finally, the controller would be called from your GSP (or whatever) page with the standard link tag:

1
<link rel="stylesheet" href="${resource(dir:'/resource/css',file:'mystyles')}" />

Adding In Some Cache Control

This is all well and good, but you did just add a little overhead to your application. Generating these files may prove costly if your application has many of them and/or they tend to get called a lot. Below is an example of the same controller above but with some cache control logic built in. I replaced some config options with hardcoded values in this example, so you may want adjust for your application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 
import java.text.SimpleDateFormat
import org.apache.http.impl.cookie.DateUtils
 
class ResourceController {
 
    def groovyPagesTemplateEngine 
 
    def css = {
        def file = params.id
 
 
        def cssPath = servletContext.getRealPath("css/${file}.css")
 
        def resourceFile = new File(cssPath)
 
        if (!resourceFile.exists()) {
            render(text:"File Not Found",status:404)
            return
        }
 
        def ifModifiedSince = request.getHeader("If-Modified-Since")
                                    // An example of a config option for enabling/disabling the cache control
        if (ifModifiedSince) { // && grailsApplication.config.resourceController.useCacheControl) {
 
            String[] formats = [DateUtils.PATTERN_ASCTIME, DateUtils.PATTERN_RFC1036, DateUtils.PATTERN_RFC1123]
            def dt_ifModifiedSince = DateUtils.parseDate(ifModifiedSince, formats)
 
            long lastMod = (long) (resourceFile.lastModified() / 1000)
            long isModSince = (long) (dt_ifModifiedSince.getTime() / 1000)
            if (lastMod <= isModSince) {
                render (status:304)
                return
            }
 
        }
 
        // An example of a config option for enabling/disabling the cache control
        //if (grailsApplication.config.resourceController.useCacheControl) {
 
            def now = new Date(System.currentTimeMillis() + (2693000L * 1000))
 
            response.setHeader("Expires", DateUtils.formatDate(now, DateUtils.PATTERN_RFC1123))
            response.setHeader("Cache-Control", "public")
            response.setHeader("Vary", "Accept-Encoding")
        //}
 
        response.addHeader("Last-Modified", DateUtils.formatDate(new Date(resourceFile.lastModified()), DateUtils.PATTERN_RFC1123))
 
        def buffer = null
        try {
            buffer = processTemplate(resourceFile, params)
        } catch (Exception ex) {
            log.error "Failed to process template", ex
        }
 
        if (buffer != null) {
            render (text:buffer, contentType:"text/css")
        } else {
            render(text:"Failed to process template", status:500)
        }
 
 
    }
 
 
    def processTemplate(resourceFile, model)
    {
 
        def buffer = resourceFile.getText()
        def template = groovyPagesTemplateEngine.createTemplate(buffer,"${resourceFile.getPath()}")
        def writer = new StringWriter()
        template.make(model).writeTo(writer)
        return writer.toString()
    }
 
}

The new steps in the example are:

  1. Get the If-Modified-Since request header and compare it to the last modified date of the css file. If the file has not been modified, return a status 304 Not Modifed. (lines 22-36)
  2. Set the cache control headers, including the Expires header for some time in the future. (lines 38-48)

Conclusion

This can be a powerful addition to your application and provide significant additional flexibility to your stylesheets. Also, the same strategy (and controller with little modification) can be used for any number of other normally static files such as Javascript, images and media.

Enjoy!
— Kevin

Leave a Reply