Creating a better PathMatcher for Spring 3

Spring 3 has excellent support for mapping URLs to @Controller methods through the @RequestMapping annotation. This works quite well and i especially like the fact having the mapping right next to the method and not in some other config file like routes.rb.

My goal was to have urls like http://foobar.com/resource, http://foobar.com/resource.html, http://foobar.com/resource.zip etc. This is no problem at all thanks to the ContentNegotiatingViewResolver.

The solution has only one draw back: The format is not known to the controller. Yes, this shouldn’t be a controller concern in most cases but what if you have a format that you don’t want to be available to all users? Maybe an nice zip download of your resources? Handling authentication in a view? I don’t think so.

So my first attempt looked like this

@RequestMapping("/resource.{format}")
public String resource(
		final @PathVariable String format,
		final HttpServletRequest request,
		final Model model
)

That didn’t work because it wouldn’t work for the default text/html resource http://foobar.com/resource so i added

@RequestMapping("/resource")
public String resource(
		final Model model
) {
  this.resource('html', model);
}

That worked for http://foobar.com/resource but not for http://foobar.com/resource.zip… “format” was always html. Hmmm…

After much googling and reading StackOverflow.com i found the “useDefaultSuffixPattern” option on org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping. If set to true (which is the default) the mapping “/resource/” will also map to “/resource/” and “/resource.*”. Although both useful i tried disabling it through my spring-cfg.xml like

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    	<property name="order" value="0" />
    	<property name="useDefaultSuffixPattern" value="false" />
</bean>

Enter the next problem: First, i didn’t work either. Second, all urls where mapped twice. Without the default suffix pattern and with. I spend 2 hours trying to locate the place where the spring config was loaded twice. In the end it was that one line that caused me trouble:

  <mvc:annotation-driven/>

That tag enables a lot of stuff in Spring 3, like the @Controller programming model and many other goodies. What it also does is establishing an AnnotationHandlerMapping that cannot be overwritten. So the next thing i did was browsing through the Spring sources to see what it does and redid with Spring beans in my config file (code follows later).

With that implemented, my urls still didn’t work, for all cases the URL without .{format} was called.

As i was already deep down in the Spring sources i had a look at the default path parser and matcher called AntPathMatcher. There is nothing wrong with the parsing code but the “getPatternComparator” method that “Given a full path, returns a Comparator suitable for sorting patterns in order of explicitness.” had some flaws, at least for my use case.

It sorts the patterns by explicitness and that explicitness is (among others) defined by how many placeholders for path variables are present. So my “/resource” is more explicit that “/resource.{format}”. With that in mind, i extend the path matcher like so:

This PathMatcher delegates most of his methods to the default AntPathMatcher but overwrites the getPatternComparator. If you have a look at the sources you’ll see that it is also partly copied. In the last else branch you’ll see that i sort both patterns by length, strip the default suffix (.*) and check wether the longer pattern starts with the other one. If it does i check wether the difference is just a .{format} (hardcoded). If that’s true, than the pattern with the format suffix is more explicit. Otherwise, i’ll use the default algorithm.

To get this to work, you cannot use the mvc:annotation-driven tag as the PathMatcher is a property of the AnnotationMappingHandler which in turn cannot be overwritten. So to get the same functionality like in Spring 3.0.5 with my PathMatcher use

As you can see i left the useDefaultSuffixPattern option enabled as it works very well with my PathMatcher and i didn’t want to care about mapping “/resource”, “/resource/” etc…

I really hope that the gists will save someone some time. I cannot imagine that i’m the only one having this kind of requirement. The solution is really simple but the way to it was not that easy.

| Comments (1) »

09-Mar-11


All roads lead to Rome…

…or: Adding Atom links to an RSS feed generated by ROME.

I’m using ROME to create RSS and Atom feeds for a project.

While ROME has excellent support for creating either RSS or Atom feeds, there is no build-in immediate support for Atom elements inside an RSS feed like

<atom:link href="http://example.com/example.rss" rel="self"/>

The first thing nowadays is to Google some keywords and finding someone who had the same problem. I found jettro and started with his solution but to me, the solution might work but is wrong.

Some points:

  • The copyFrom method means exactly the opposite
  • The generator doesn’t need to add the namespace
  • I want to add more than a link some time

I’ve implemented an AtomContent class that holds a list of com.sun.syndication.feed.atom.Link but is easy extensible.

This content is managed by an AtomModule like so

public interface AtomModule extends Module {
	/** The public namespace URI */
	public final static String ATOM_10_URI = "http://www.w3.org/2005/Atom";
	/** as used in the namespaced prefixed with "atom" */
	public final static Namespace ATOM_NS = Namespace.getNamespace("atom", ATOM_10_URI);
 
	/** Gets the included content */
	public AtomContent getContent();
 
	/** Sets the included content */
	public void setContent(final AtomContent content);
}

This interface is accompanied by an implementation AtomModuleImpl that is used to provide instances of AtomContent and especially an AtomModuleGenerator that is used to generate XML elements via JDOM.

The generator implements ModuleGenerator and if added through a rome.properties files automatically adds the appropriate namespace to the feeds. The rome.properties looks like this:

rss_2.0.feed.ModuleGenerator.classes=ac.simons.syndication.modules.atom.AtomModuleGenerator
rss_2.0.item.ModuleGenerator.classes=ac.simons.syndication.modules.atom.AtomModuleGenerator

As you see, i’ll only added the generator to RSS 2.0. Also a parser is not available at the moment as i didn’t need one.

The following snippet demonstrates the usage of the AtomModule:

final AtomContent atomContent = new AtomContent();
atomContent.addLink(new SyndicationLink().withRel("self").withHref("http://example.com/example.rss").getLink());
atomContent.addLink(new SyndicationLink().withRel("alternate").withType("text/html").withHref("http://example.com/example.html").getLink());
feed.getModules().add(new AtomModuleImpl(atomContent));

I published the code as java-syndication along with some other helper classes. If i need more elements, i’ll add them, but in the meantime, the code is usable and maybe of some value for people building RSS feeds with Java and ROME.

| Comments (0) »

14-Feb-11


Server push, jquery atmosphere and the throbber of doom

I’m using the Atmosphere framework to implement server push (a.k.a. Comet) in a Spring 3 application.

Atmosphere includes a fine jQuery plugin to handle the client side code. The plugin works quite well and Jean-Francois, the author, has a good tutorial in his blog: Using Atmosphere’s JQuery Plug In to build applications supporting both Websocket and Comet.

The code works but causes webkit browsers (Chrome and Safari) to show the throbber (that little spinning thingy in tab or address bar). In iOS browsers that loading progress bar will not disappear.

I found some suggestions to get rid of the “throbber of doom” but none of them worked for me.

So here is my solution which is tested with jQuery 1.4.2 and jQuery.atmosphere 0.6.3:

<script>  	
	// See https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
	if(!Function.prototype.bind) {
		Function.prototype.bind = function( obj ) {
			var slice = [].slice,
			args = slice.call(arguments, 1), 
			self = this, 
			nop = function () {}, 
			bound = function () {
				return self.apply( this instanceof nop ? this : ( obj || {} ), args.concat( slice.call(arguments) ) );    
			};
 
			nop.prototype = self.prototype;
			bound.prototype = new nop();
			return bound;
		};
	}
 
	var subscribe = function() {		 	
		$.atmosphere.subscribe(
			'/subscription/url',
			function(response) {				
				$('body').append(response.responseBody + '<br>');						
			},
			$.atmosphere.request = {								
				transport: 'websocket',
				fallbackTransport : 'long-polling'
			}
		);
	};
 
	$(window).load(function() {				
		setTimeout(subscribe.bind(document),500);
	});
</script>

The trick is: Wait for the whole page to be loaded (including all resources) by using $(window).load instead of $(document).ready. Then set a timeout to your subscription function. The timeout will change the execution context of the function to the global object (in most cases window). This needs to be corrected to the document for the atmosphere jQuery plugin to work. Here comes bind() to the rescue. This is a Javascript 1.8.5 function and it is not available in all browsers (see bind) and therefore added to the prototype.

So far i’ve tested this in Firefox 3.6, Chrome 8, Safari 5 and Internet Explorer 8 and had no problems.

| Comments (0) »

03-Feb-11


Disable jsessionid path parameter in Java web applications

Wow, this has driven me nuts…

Most J2EE developers will know the ugly-as-hell ;jsessionid=BLAHBLAHBLAH appended as a path parameter to all urls of an application on the first call of a page that creates a session.

Tomcat as of version 6 has the possibility to add the attribute ‘disableURLRewriting=”true”‘ to the context of the application but that didn’t work for me…

The problem was: I’m using Spring Security and Spring Security has it’s own mechanism and control for the session store.

It can be disabled through security-conf.xml (or wherever you’ve stored the Spring Security configuration) within the http element:

<http use-expressions="true" disable-url-rewriting="true" create-session="ifRequired">

And boom, the path parameter is gone.

For more information see The Security Namespace

The jsession parameter is used on the first page that requires a session as the server cannot now at this point in time whether the client supports cookies or not. If you disable it, you’re clients need to allow cookies, otherwise no session will be created.

| Comments (3) »

28-Jan-11


Migrate OS X to a bigger hard disk

I wanted to upgrade my 2008 MacBook (the aluminium one without firewire) with a bigger and especially faster hdd. I chose a Hitachi Travelstar 7k500. So far, it was a good choice.

This tip does only apply to an HFS+ formatted drive with a GUID partition table.

I already had a (bootable) backup that i create using SuperDuper! But for the migration purpose i wanted a bitwise copy of the old disk, so i used dd. I started the MacBook with the external drive containing the bootable backup and some space for the disk image (as i had no external SATA-USB adapter at hand. The (simple) commands for cloning (and zipping) the internal disk to an image are:

dd bs=128k if=/dev/disk0 | gzip > /path/to/image.gz

The internal disk is in most cases /dev/disk0 but it can vary. The process took about 7hours, i blame my old and slow external disk for that.

After switching the internal disk, which is irritating simple, i don’t know why Apple had to change that in the newer unibodies, i restored the image with

gzip -dc /path/to/image.gz | dd of=/dev/disk0

and rebooted.

The Mac came back immediately without a problem. If you don’t know dd you we’ll be surprised what you see: Your new disk and the partition on it has exactly the same free space as before. This is because dd does an exact bitwise copy of your disk and doesn’t resize the partition.

With the build-in OS X “Disk Utility” (“Festplattendienstprogramm”) you can easily resize the one partition (or create a second one if you like), altough you must boot from the OS X installer disk or as i did, from another bootable backup.

And there was the only problem i had. The german error message says

Partitionieren ist fehlgeschlagen

Beim Partitionieren ist folgender Fehler aufgetreten:

MediaKit meldet: Partition (Tabelle) zu klein

The english message is

Partition failed

Partition failed with the error:

MediaKit reports partition (map) too small.

Bummer. The filesystem itself is resizable but the partition table itself is not. When the original drive was formatted, the partition table was created just for that device.

I found the solution in the Life as i know blog. The author has the same problem while expanding a raid, but the solution can be used without problems for a simple drive. Note that this works only if the drive has just one partition on it! If you try this on your own, i’m not responsible for any dataloss.

Just use the following commands in a terminal (replace /dev/disk0 with your device) (the device must not be mounted while using the commands, therefor like stated above, boot from the installer disk or a backup):

sudo gpt show /dev/disk0
This shows the current partition table. Note the number in the column “SIZE” with the highest value.
Unmount the disk through Disk Utility
The Partitiontable cannot be changed when the drive is mounted.
sudo gpt destroy /dev/disk0
This destroys the current partition table, so be carefull.
sudo gpt create -f /dev/disk0
This creates a new partition table with the correct size for your new harddrive
sudo gpt add -b 409640 -s XXX /dev/disk0
Replace the XXX with the value from size in the first step.
Use Diskutility to mount the disk again. You now can resize the partition or add another
Eventually you need to reboot, but in my case it wasn’t necessary, the partition showed up immediatly. I hower did reboot after resizing it to see, wether the cloning did work.

The cloning did work and i now have a much faster system and more space available.

You can use the process of destroying and recreating the partition table for drives with more than one partition, you must repeat the gpt add step and change the start and end parameter correctly.

Please be sure what you do, when you try to repeat the tipps in this post!

| Comments (9) »

19-Nov-10