SiteMesh 2 Undecorated AJAX requests

Note: Only after testing and writing this did I realize this is not a good idea. I will leave this up in the case it helps someone do something else, but this is a poor way to do this. I will update it later with a beter technique.

Recently I had the problem where I wanted my JavaScript requests to return undecorated content. The solutions I found online weren’t quite what I wanted so I constructed my own method using a custom mapper. The first two solutions below are the once I came across in my searches, the third is mine.

API Path

One simple solution is to keep all requests which you want undecorated in a secondary path, such as /api, and set your decorators.xml to exclude them all from decoration.

<excludes>
   <pattern>/api**</pattern>
</excludes>

Parameter Patterns

The request posted at http://issues.appfuse.org/browse/APF-1020 made the suggestion to add a key-value parameter pair such as ajax=true to every request you want undecorated. Inelegant and not transparent at all, but effective. See http://raibledesigns.com/rd/entry/ajaxified_body for a little more detail.

Header

Instead of doing excludes on URL pattern matching, I wanted every javascript request to be undecorated by examining the X-Requested-With header. To implement this, ensure you have a custom sitemesh.xml.

I create a blank decorator, this is my /WEB-INF/decorators/api.jsp

<%@ taglib prefix="decorator" uri="http://www.opensymphony.com/sitemesh/decorator"%><decorator:body />

And ensure that you declare this as a decorator within your /WEB-INF/decorators.xml (or wherever you might have put it)

<decorators defaultdir="/WEB-INF/decorators">
    <decorator name="api" page="api.jsp" >
        <pattern>/api*</pattern> <!-- This line is OPTIONAL -->
    </decorator>
</decorators>

and the last XML modification is to insert another custom mapper into the sitemesh configuration. If you don’t have one, simply create /WEB-INF/sitemesh.xml populated with the default content from sitemesh-default.xml.

...
   <decorator-mappers>
      <mapper class="com.your.package.structure.mapper.HeaderDecoratorMapper">
         <param name="decorator" value="api" />
         <param name="X-Requested-With" value="XMLHttpRequest" />
      </mapper>
   </decorator-mappers>
...

Notice the custom class we map to. This is the class that will inspect the headers for the correct values and respond accordingly. Here is HeaderDecoratorMapper.java:

package com.your.package.structure.mapper;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;

import com.opensymphony.module.sitemesh.Config;
import com.opensymphony.module.sitemesh.Decorator;
import com.opensymphony.module.sitemesh.DecoratorMapper;
import com.opensymphony.module.sitemesh.Page;
import com.opensymphony.module.sitemesh.mapper.AbstractDecoratorMapper;

public class HeaderDecoratorMapper extends AbstractDecoratorMapper
{
	private final Logger logger = Logger.getLogger(HeaderDecoratorMapper.class);

	private Map<String, String> headerMap = null;

	public void init(Config config, Properties properties, DecoratorMapper parent) throws InstantiationException
	{
		super.init(config, properties, parent);
		headerMap = new HashMap<String, String>();
		initMap(properties);
	}

	public Decorator getDecorator(HttpServletRequest request, Page page)
	{
		try
		{
			Decorator result = null;

			final List<String> headers = Collections.list(request.getHeaderNames());
			for(final String header : headerMap.keySet())
			{
				if(headers.contains(header) && request.getHeader(header).matches(headerMap.get(header)))
				{
					// We know we want to differ decorators
					if(logger.isDebugEnabled()) {
						logger.debug("Decorating header request with the decorator: " + headerMap.get("decorator"));
					}
					result = super.getNamedDecorator(request, headerMap.get("decorator"));
					break;
				}
			}

			return result == null ? super.getDecorator(request, page) : result;
		}
		catch (NullPointerException e)
		{
			return super.getDecorator(request, page);
		}
	}

	/** Initialize the header mappings. */
	private void initMap(final Properties props)
	{
		final Iterator<Entry<Object, Object>> it = props.entrySet().iterator();
		while (it.hasNext())
		{
			final Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) it.next();
			final String key = (String) entry.getKey();
			final String ext = (String) entry.getValue();
			headerMap.put(key, ext);
			if(logger.isDebugEnabled()) {
				logger.debug("Header mapping '"+key+"' with value '"+ext+"' ");
			}
		}
	}
}

With all of these in place you should be able to redirect any header key:value combination, with the value of the value being regex matched, to any view you want.

Thanks! Hopefully this helps.

 

This entry was posted in uncategorized. Bookmark the permalink.

2 Responses to SiteMesh 2 Undecorated AJAX requests

  1. Tribul says:

    “Only after testing and writing this did I realize this is not a good idea”

    You currently have three ideas on this page – which are you referring to?

  2. Tribul says:

    and what was the downside that you discovered?

Leave a Reply

Your email address will not be published.