Struts SitemapProcessor

Cool!

From the very first minute I started playing with Cocoon, I was completely in love with their sitemaps. Any URL mapped arbitrarily to back-end logic via RegEx expressions. It works perfectly and is, IMHO, how all future App Servers will be developed in the future. Of course, I decided to roll my own XML-based web-server solution (like Bob, I have a severe case of Chronic Reimplementor Syndrome) so that means I've been coveting Cocoon's sitemap ever since.

Well, I finally got around to re-implementing a portion of what Cocoon does in Struts with a custom RequestProcessor. It's very simple at the moment, but it does exactly what I want at the moment. This stuff in my opinion, would sit better in the struts.config file, where the actions's path attribute could be a RegEx expression, but here's a hack until it gets integrated.

The part that's not integrated just yet is the incorporation of Client-detection with DELI or WURFL, but that's coming next. It'll probably just be a call to another class to analyze the headers, then a variable thrown into the request object for use by the Actions when they're choosing a view to go to.

Below is the code, leave me comments here if you're interested in getting it working (don't email me questions, let everyone help):

package com.manywhere.struts;

import java.io.*;
import javax.servlet.http.*;
import org.apache.struts.action.*;
import org.apache.struts.config.*;
import javax.servlet.*;
import java.util.*;
import java.util.regex.*;
import org.jdom.*;
import org.jdom.input.*;

public class SitemapProcessor extends RequestProcessor{

        private static final String SITEMAP_PATH = "/WEB-INF/sitemap.xml";

        private HashMap actionMap = new HashMap();
        private HashMap patternMap = new HashMap();
        private int mapCount = 0;

        public void init(ActionServlet servletConfig, ModuleConfig moduleConfig)
                   throws ServletException 
        {

                super.init(servletConfig, moduleConfig);

                try {

                        SAXBuilder builder = new SAXBuilder();
                        InputStream is = servletConfig.getServletContext().getResourceAsStream(SITEMAP_PATH);
                        Document doc = builder.build(is);
                        Element root = doc.getRootElement();

                        List actions = root.getChildren();

                        Iterator i = actions.iterator();

                        while (i.hasNext()) {
                                Element action = (Element) i.next();

                                String actionStr = action.getTextTrim();
                                String patternStr = action.getAttributeValue("match");

                                mapCount++;
                                Integer mapCountInt = new Integer(mapCount);

                                actionMap.put(mapCountInt, actionStr);
                                Pattern pattern = Pattern.compile(patternStr);
                                patternMap.put(mapCountInt, pattern);

                        }                       

                        is.close();

                } catch (Exception e) {
                        throw new ServletException("Error loading sitemap.", e);
                }       

        }

        protected String processPath(HttpServletRequest request,
                                                                 HttpServletResponse response)
                throws IOException 
        {

                        String path = request.getServletPath();

                        for(int x=1; x < mapCount; x++) {

                                Integer mapCountInt = new Integer(x);

                                Pattern pattern = (Pattern) patternMap.get(mapCountInt);

                                Matcher matcher = pattern.matcher(path);
                                boolean matchFound = matcher.find();

                                if(matchFound) {

                                        ArrayList matchList = new ArrayList();
                                        for (int i=0; i <= matcher.groupCount(); i++) {
                                                matchList.add(matcher.group(i));
                                        }
                                        request.setAttribute("matchList", matchList);
                                        request.setAttribute("originalPath", path);

                                        return (String) actionMap.get(mapCountInt);

                                }

                        }

                        return super.processPath(request, response);

         }

}

To use it, you edit your web.xml file in your WEB-INF directory and point the URL patterns you want to control from Struts to the ActionServlet. Normally, this means just "*.do" or "/do/*", but now you want to also point other url patterns like *.html or *.xml or "/files/*".

Now you need to create a sitemap.xml file in your WEB-INF directory (yes it's hard coded, sue me.) it should look something like this:

<?xml version="1.0"?>

<sitemap>

        <action match="/index.html">
                /indexAction
        </action>

        <action match="/comments.html">
                /commentsAction
        </action>

        <action match="/([0-9][0-9][0-9][0-9][0-9][0-9][0-9]).html">
                /pageAction
        </action>

        <action match="/([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).html">
                /indexAction
        </action>

        <action match="/(.*).html">
                /notFoundAction
        </action>

        <action match="/(.*).xml">
                /notFoundAction
        </action>

</sitemap>

The way it works is that the SitemapProcessor pulls in the RegEx expressions and matches them to URLs that pass through it. If it matches, it'll pass that Action URL for processing, if it doesn't match, it'll pass through to the default actions, which are normally your /do/* actions. The only real kludge to this is the /notFoundAction which you need to differentiate between a bad URL and a request for the default URLs. In my server the NotFoundAction simply returns a 404.

For added fun, I've also thrown the matched groups from the RegEx query into the request object in an attribute called "matchList", so you can use the parameters from your Action, like this:

package com.manywhere.action;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.jdom.*;
import org.jdom.input.*;
import org.jdom.xpath.*;
import com.manywhere.xao.*;
import java.util.*;

import org.apache.struts.action.*;

public class PageAction extends BaseAction {

  public ActionForward execute(ActionMapping mapping, ActionForm form, 
                        HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

                List matchList = (List) request.getAttribute("matchList");

                String id = (String) matchList.get(1);

                if(id == null || id.trim().equals("")){
                        request.setAttribute("errorMessage", "No ID found");
                        return mapping.findForward("error");    
                }

                XMLMap params = new XMLMap();
                params.put("action", "getEntryById");
                params.put("id", id);

                try {

                        EntryXAO entryXAO = new EntryXAO();

                        String xml = entryXAO.getXML(params);

                        request.setAttribute("xml",xml);

                        SAXBuilder saxBuilder = new SAXBuilder();
                        Document doc = saxBuilder.build(new StringReader(xml));

                        XPath xpath = XPath.newInstance("/document/entry/title");
                        String title = xpath.valueOf(doc);

                        request.setAttribute("title", title);

                } catch (Exception ex) {

                        ex.printStackTrace();
                        return mapping.findForward("errorPage");

                }

                return mapping.findForward("indexPage");        

        }

}

Okay, that's what I've been working on. Your comments welcome.

-Russ

< Previous         Next >