ResourceTransformFilter is a swiss-army-knife for HTML/CSS/Javascript transformation in Java web applications.
It’s a new addition to the visural-common Apache 2.0 licensed project.
Apply this filter in your Java webapp to get -
- Automatic CSS DataURI image inlining for supported browsers
- Automatic CSS MHTML image inlining for supported IE browsers
- LessCSS compilation for .less files
- Automatic YUI compression of .css & .js resources
- Optional: DataURI inlining of <img> tags in HTML responses
So as you can see, the primary purpose of this filter is web app resource optimization.
DataURIs and MHTML?
One of the most effective ways of improving website performance is to reduce the number of HTTP requests required for the page to fully load.
Typically you’ll have a bunch of small icons used for buttons, panels, links etc. in your CSS, as well as small background graphics here and there. These all add us to a significant number of HTTP requests, each one for only a couple of kilobytes of data.
“CSS Sprites” have often been used as a way of overcoming this issue, however two newer techniques (data URIs and MHTML) are starting to become a more common way to address the problem.
The idea of data URIs and MHTML are the same – inline the image data into the actual CSS (or HTML) as base64-encoded text.
Data URIs are the preferred way of doing this, however they are not supported in older browsers, specifically IE6 and IE7. Also, IE7 on Windows Vista has problems, and I’ve talked about this in more detail recently. MHTML can be used reliably in IE6/7 on Windows XP, and so the filter will apply it for those clients.
Applying the Filter
I’ll cut right to the chase – here’s how you can apply this to your Java web-application in two simple steps:
- Add visural-common to your projects set of libraries, or adding it as a dependency (Maven). You will also need to add the YUI Jar to your project to use CSS / JS compression.
- Add com.visural.common.web.transform.ResourceTransformFilter to your your web.xml as a standard servlet filter, matched to the URLs of your resource file(s)
For example you might add the following XML your web.xml:
<filter>
<filter-name>ResourceTransformFilter</filter-name>
<filter-class>com.visural.common.web.transform.ResourceTransformFilter</filter-class>
</filter>
...
<filter-mapping>
<filter-name>ResourceTransformFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
And you’re done.
Default Configuation
The default configuration works as follows -
- Supported features will be detected by using visural-common’s built in WebClient detector (based on User-Agent)
- Resource URLs ending in .less will be compiled through the LessCSS compiler
- Resources ending in .less or .css will have DataURIs or MHTML applied, but only if the client supports them, otherwise the original image references will be left untouched.
By default, only images < 50kb in size will be inlined, any larger images will be left as url(..) references. This is configurable. - Resources ending in .less or .css will be compressed through the Yahoo YUI CSS Compressor
- Resources ending in .js will be compressed through the Yahoo YUI Javascript Compressor
Transformed responses will be automatically cached, so any additional overhead to generate these responses will be a one-time only cost. Note that any response headers you set will also be cached, and the filter does not affect the detection and dynamic behaviour based on the client browser/OS.
Custom Configuation
You can customise the behaviour of the filter by extending the filter’s class com.visural.common.web.transform.ResourceTransformFilter and overriding the protected “over-ride points” to change the classes behaviour.
Use the source as the starting point.
Optional .html DataURI Conversion
The default configuration does not do auto-conversion of HTML <img> tags to DataURI’s.
The reason for this is, that it is not always possible to reliably detect HTML responses, without a performance cost, and additionally, the CPU, memory and bandwidth overhead to doing this can be undersirable.
Unlike .CSS files, HTML responses are typically dynamic and can’t be cached. Additionally, images in the response may be from external servers, and may be larger than the types of image typically referenced from CSS.
For this reason, you will need to evaluate your circumstances to determine if enabling HTML inlining is a good idea in your application.
If you do want to enable it, you could do so like in the following example -
import com.visural.common.web.client.WebClient;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
public class MyResourceTransformFilter extends com.visural.common.web.transform.ResourceTransformFilter {
@Override
protected List getTransforms(HttpServletRequest req, WebClient client) {
String url = req.getRequestURI();
String last = url.substring(url.lastIndexOf('/'));
if (last.contains(".")) {
return super.getTransforms(req, client);
} else if (client.supportsDataUris()) {
return Arrays.asList(Transform.HTML_DATAURI);
} else {
return null;
}
}
}
There are several other override points, included in the filter, so consult the source or drop me an email if you’d like to customise further.
Summary
So with this one simple addition we get framework-agnostic image inlining, compilation and compression which responds intelligently to the user’s choice of browser.
This has been live on onmydoorstep.com.au for a week or two is working out well thus far.
Great, I don’t use servlet filters!
There is also a command-line version of the DataURI and MHTML converters which you can apply to non-JVM based projects as part of a build-script or manual process.
If you’ve downloaded the commandline version you can run it by:
java -cp visural-common.jar com.visural.common.web.css.CSSDataUri [input-file.css] [output-file.css]
This will convert the [input-file.css] and inline the image data into the output. Note that the relative URLs are assumed to be resolvable relative the location of the CSS on disk. Mostly this will work just fine, and it will even attempt to resolve http:// urls.
The one thing that won’t work with the command line version is root-relative urls, e.g. url(“/folder/myimage.png”), since there’s no way to know what the root is, in the context of the disk. I could fix this is a future version by adding another command line option, so if you need it, let me know.
Similarly, you can run the MHTML converter from the command-line as so:
java -cp visural-common.jar com.visural.common.web.css.CSSMHTML [input-file.css] [output-file.css] [http://serverurl/for/css]
For this one, you need to supply the additional server URL for the CSS file. This is because MHTML file references need the full URL to the defining CSS file (relative URLs don’t work).
This is one of the reasons that I chose to implement the solution as a filter.
Anyhow, given the simplicity of this filter, there’s pretty much no reason not to use this in your Java web app and improve those YSlow / PageSpeed results and rating without very little effort
Related posts:
Elegant, unobtrusive, AND powerful ! Great job !
For resource aggregation and compression I use JAWR. This is nice though.
Very nice. Have been thinking about something like this for a while.
I need to disable the filter when doing development, so I created a version that does nothing if the system property \developermode\ exist.
The source with a pom-file that makes the project build with maven can be found here.
http://underdusken.no:8080/nexus/content/repositories/thirdparty/com/visural/visural-common/0.4.2
Hi Marvin,
You could have implemented your change like in the following code, avoiding having to fork the whole code base -
e.g.
public class MyResourceTransformFilter extends com.visural.common.web.transform.ResourceTransformFilter {
@Override
protected List getTransforms(HttpServletRequest req, WebClient client) {
if(System.getProperty("developermode") != null){
return null;
else {
return super.getTransforms(req, client);
}
}
}
Then add MyResourceTransformFilter to your web.xml instead of the original ResourceTransformFilter.
hmm.. Yeah, that would have been better.
Will do that next time
Great tut Rich! I followed your instructions but the .less files don’t get compiled. I tested it by loading the JS compiler instead and it worked. Apart from step 1 and 2 above, what else should I check? since the filter handles the compiling of the .less files there is no need for the JS compiler, correct?
I’d really appreciate your help.
thanks.
@jCastillo
result)” method. You get passed the current url and the list of transforms (to which you can add the LessCSS if you wish).
Hmmm, does your URL end in “.less”? That is the way the filter determines (by default) whether to apply the LessCSS transform.
You can sub-class the filter in your own project and override the “protected void addLessCSS(String urlLower, List
Check the code here.
Thanks @Rich for the quick response. We trap *.css and *.js with JAWR but the filter is set for *.less
I really don’t know why it doesn’t do anything but I did notice that when I use the JS compiler the renders with error classes inside of it.
All the files are tag at the end with expiration on the way down but I figured this would affect anything since the filter should get to the .less file first.
Let me try the subclass approach and hopefully that will work.
Why does LESS ads the CSS as embedded style instead of attaching a style sheet? is there a way to fix this?
thanks.
Not sure what you mean by “embedded style”? Do you mean the resource inlining with Data URIs? That can be disabled in the same way, but sub-classing and override the inline method to remove the transform (there are methods for lesscss, javascript compression, css compression and datauris).
Maybe if you’re still having problems, email me your web.xml file and I can help you over email: rn at visural.com
Hey Richard,
I’ve been using your filter for LESS processing among other things, and I’m struggling a bit with @import. I can’t seem to figure out a path that works that doesn’t result in:
“java.lang.IllegalStateException: LessCSS failed: @import “/styles/core.less”;
Any points to what might work?
@Jon Currently my implementation doesn’t support the @import directive.
This is something that I’ve been planning on looking at for a while, but haven’t had the time as yet.