« May 2007 | Main | September 2007 »

June 13, 2007

Dojo drag and drop gotcha in IE

I have been working on a web app for one of our clients that includes a pretty interactive AJAX / Web 2.0 component within it. This component is essentially a simplified drag-and-drop book publishing tool, which allows users to upload images and then to arrange them on predefined templates in order to create a book.

We chose to use the Dojo Toolkit (version 0.4.2) for both the AJAX (ie: remote calls to the server) and the Widegtry side of the Web 2.0 style. It's been working fairly well. Dojo has a lot of functionality, but also has a few areas that still need some refinement. The most interesting / infuriating to me so far has to do with Dojo drag-and-drop functionality within IE.

Dojo's facility for drag-and-drop is essentially composed of three components:

  • Drag-and-Drop Manager
  • Drag Sources
  • Drop Targets

It intuitively makes a lot of sense. You create drag sources and drop targets and out of the box they actually work pretty well. Dojo uses a form of JavaScript inheritance to allow you to create drag copy sources or drag move sources, each of which have different behaviors. You can also reassign specific functions on your drag sources or drop targets in order to customize their behaviors.

I developed and debugged the app in Firefox and Firebug, as we all do. But when it came time to check that everything works in IE, I ran in to one bug in particular that stumped me for two entire days, and frustrated me to no end. At a certain point in using the app, my ability to drop objects on their drop targets was broken. It only happened in IE (I was using IE 6 for Windows), and the error I got was the always helpful "Unexpected error".

What I found was that the following code, which creates a drop target, also registers the drop target with the Drag and Drop Manager.

var imageTarget = new dojo.dnd.HtmlDropTarget(imageDiv, ['galleryImages']);
imageDiv.imageTarget = imageTarget;

As a matter of fact, the Drag and Drop Manager, which is a global singleton, keeps a registry of all drag sources and drop targets created. Later on in the flow of my application, some of these drop targets need to be re-rendered, and to do so I actually delete the DOM node and re-create it, using the following code:

dojo.dom.destroyNode(pageContainerDiv);

The destroyNode() function is supposed to be memory-leak-friendly. But after I have done that, and created a new drop target where that one once was, when I try to drop another drag source on to it, I get the "Unexpected error" exception in JavaScript. And again, this only happens in IE. Firefox works fine. I finally realized that, even though the drop targets are automatically (and outside my knowledge for a long time) being registered with the Drag and Drop Manager, they must be explicitly unregistered before they are destroyed, otherwise this error occurs. I added the following code to do so.

dojo.dnd.dragManager.unregisterDropTarget(imageDiv.imageTarget);

And now everything works perfectly!

Posted by Mike Wynholds at 10:21 AM | Comments (1)

June 11, 2007

Parameterized REST URLs with Spring MVC

At Carbon Five, we've been working REST-ful practices into our web applications for some time now. Providing simple URLs for application entities is a key principal of this style, but parsing parameters out of the request path has been klunky in Spring MVC. Spring's WebFlow apparently supports REST-ful URLs, but I've never found anything in what I've read or heard to recommended that project (though I've heard nothing bad).

I finally got fed up with the situation and worked out a solution that lets developers specify path parameters in the dispatcher mappings, which will appear as request parameters in the controller and view. I've made the project available on our public SVN server. The solution requires only 4 classes, so you can download them instead. Read on for configuration information.

ParameterizedUrlHandlerMapping

The solution is to replace Spring MVC's SimpleUrlHandlerMapping with ParameterizedUrlHandlerMapping. This class is responsible for routing requests to the appropriate handler (servlet, controller, or JSP), so it's an ideal place to specify the parameters. Of course, at this point, it has no more access to the request than a controller, so the best it can do is add the path parameters to the request attributes. It adds them as a Map of String name/value pairs with the key "ParameterizedUrlHandlerMapping.path-parameters".

To use ParameterizedUrlHandlerMapping, replace the SimpleUrlHandlerMapping bean in the dispatcher configuration file with something that looks like this:

 <bean class="carbonfive.spring.web.pathparameter.ParameterizedUrlHandlerMapping">
   <property name="alwaysUseFullPath" value="true"/>
   <property name="mappings">
     <props>
       <prop key="/view/noparameters">controller1</prop>
       <prop key="/view/(bar:foo)">controller2</prop>
       <prop key="/view/(*.html:html)">controller3</prop>
       <prop key="/view/(**/*:view).view">controller4</prop>
       <prop key="/view/c/(*:controller)/(*:id)">controller5</prop>
     </props>
   </property>
 </bean>

The ParameterizedUrlHandlerMapping supports all mappings that are valid using SimpleUrlHandlerMapping's default AntPathMatcher. The special parenthetical sections of the patterns have the syntax:
'(' + [ant_style_path] + ':' + [parameter_name] + ')'
Any part of the path pattern can be within parenthesis. The above example will having the following effect:

pathcontrollerparameters
/view/noparameterscontroller1
/view/barcontroller2foo -> bar
/view/piglet.htmlcontroller3html -> piglet.html
/view/this/that/the-other.viewcontroller4view -> this/that/the-other
/view/c/save/2342443controller5controller -> save
id -> 2342443

ParameterizedPathFilter

If accessing the parameters from request attributes is all you need, you can stop here. But to elevate your path parameters to first class citizen status in your web app, you need them to appear as request parameters. Once they are request parameters, Spring will bind them to your command or form objects, and your controllers can once again forget the details of request parsing and focus on business logic. To complete the solution, we need the ParameterizedPathFilter configured as a web filter. It proxies the request with a wrapper class that listens for ParameterizedUrlHandlerMapping's request attribute. When it is set, the wrapper class adds the parameters to the request parameters for the remaining life of the request.

To configure the ParameterizedPathFitler, add the following to your web.xml:

  <filter>
    <filter-name>PathParameterFilter</filter-name>
    <filter-class>carbonfive.spring.web.pathparameter.ParameterizedPathFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>PathParameterFilter</filter-name>
    <url-pattern>/path/to/dispatcher/servlet/*</url-pattern>
  </filter-mapping>

You should have a filter mapping for each path pattern mapped to a Spring dispatcher servlet.

That's all it takes to add this functionality to your webapp. We've been using it in our current project. It's been problem free and the architectural improvements have been satisfying.

Posted by Alex Cruikshank at 9:45 AM | Comments (19)

June 8, 2007

Using UrlRewriteFilter with the Spring Framework

UrlRewriteFilter is a powerful and widely used Java Servlet Filter for rewriting URLs ala Apache's mod_rewrite module. We use Spring extensively at Carbon Five and I wanted to use UrlRewriteFilter with Spring-managed configuration and without having to register it in my web.xml file. Our content management system, SmileMaker, already uses Spring-managed filter chaining and I needed to add UrlRewriteFilter to the mix to give the CMS pretty URLs and to support search engine optimizations (SEO). I achieved this by writing a FilterFactoryBean that will initialize and configure a servlet filter in my Spring application context.

We typically register one Spring FilterToBeanProxy and configure our filters using FilterChainProxy. (Note: these classes referenced are actually part of the ACEGI security framework for Spring.) This keeps the web.xml simple and gives us all the power of Spring for configuring and initializing our filters. In this model, you typically use setter-based injection to configure your filter. In the case of UrlRewriteFilter, the filter must be initialized with the Filter API method init and a FilterConfig object. The problem I needed to solve was how to create and initialize this filter, or any servlet filter really, from Spring where init must be called with initialization parameters in the FilterConfig.

My solution is a fairly standard approach that you use when integrating 3rd party libraries with Spring - create a FactoryBean. The Spring FactoryBean interface provides a simple mechanism for creating objects with life cycle constraints that make them hard to create using Spring's standard mechanisms.

Here's my implementation of a FilterFactoryBean. Note that this FactoryBean could be used to initialize any servlet filter in Spring.

package carbonfive.springframework.filter;

import javax.servlet.*;
import java.util.*;

import org.springframework.beans.factory.*;
import org.springframework.web.context.*;
import org.apache.commons.logging.*;

/**
 * FactoryBean implementation for creating and initializing a servlet filter for use
 * in a Spring context. Written to handle initializing UrlRewriteFilter.
 */
public class FilterFactoryBean implements FactoryBean, ServletContextAware, InitializingBean
{
   Log log = LogFactory.getLog(FilterFactoryBean.class);
 
   private Hashtable initParameters;
   private Class filterClass;
   private ServletContext servletContext;
 
   public Object getObject() throws Exception
   {
      log.info("Creating filter " + filterClass.getName());
      
      Filter filter = (Filter) filterClass.newInstance();
      FilterConfig config = new FilterConfigImpl(initParameters);
      filter.init(config);
      return filter;
    }
 
   public Class getObjectType()
   {
      return Filter.class;
    }
 
   public boolean isSingleton()
   {
      return true;
    }
 
   public void setFilterClass(Class filterClass)
   {
      this.filterClass = filterClass;
    }
 
   public void setInitParameters(Hashtable initParameters)
   {
      this.initParameters = initParameters;
    }
 
   public void setServletContext(ServletContext servletContext)
   {
      this.servletContext = servletContext;
    }
 
   public void afterPropertiesSet() throws Exception
   {
      if (filterClass == null) throw new IllegalArgumentException("filterClass can not be null");
      if (!Filter.class.isAssignableFrom(filterClass)) throw new IllegalArgumentException("filterClass must implement javax.servlet.Filter");
    }
 
   private class FilterConfigImpl implements FilterConfig
   {
      Hashtable properties;
  
      public FilterConfigImpl(Hashtable properties)
      {
         this.properties = properties;
       }
  
      public String getFilterName()
      {
         return filterClass.getName() + "-filter";
       }
  
      public ServletContext getServletContext()
      {
         return servletContext;
       }
  
      public String getInitParameter(String name)
      {
         return (String) properties.get(name);
       }
  
      public Enumeration getInitParameterNames()
      {
         return properties.keys();
       }
    }
}

And a test for this implementation using UrlRewriteFilter:

package carbonfive.springframework.filter;

import javax.servlet.*;
import java.util.*;

import junit.framework.*;
import org.tuckey.web.filters.urlrewrite.*;
import org.apache.commons.logging.*;
import org.springframework.mock.web.*;

public class FilterFactoryBeanTest extends TestCase
{
   Log log = LogFactory.getLog(FilterFactoryBeanTest.class);
 
   public void testCreateUrlRewriteFilter() throws Exception
   {
      log.info("Starting test");
      
      FilterFactoryBean factory = new FilterFactoryBean();
      factory.setServletContext(new MockServletContext());
      factory.setFilterClass(UrlRewriteFilter.class);
  
      Properties properties = new Properties();
      properties.put("confPath", "/WEB-INF/rewrite.xml");
      properties.put("confReloadCheckInterval", "30");
      properties.put("logLevel", "DEBUG");
      properties.put("statusPath", "/status");
      properties.put("statusEnabled", "true");
      properties.put("statusEnabledOnHosts", "localhost,127.0.0.1");
  
      factory.setInitParameters(properties);
      factory.afterPropertiesSet();
      
      assertEquals(Filter.class, factory.getObjectType());
  
      Filter filter = (Filter) factory.getObject();
      assertNotNull(filter);
      assertTrue(filter instanceof UrlRewriteFilter);
  
      UrlRewriteFilter rewriter = (UrlRewriteFilter) filter;
      assertEquals(30, rewriter.getConfReloadCheckInterval());
      assertEquals(true, rewriter.isConfReloadCheckEnabled());
      assertEquals("/status", rewriter.getStatusPath());
      assertEquals(true, rewriter.isStatusEnabled());
    }
}

To create this filter in Spring my configuration looks like:

  <bean id="smilemaker-urlRewriteFilter" class="carbonfive.springframework.filter.FilterFactoryBean">
    <property name="filterClass" value="org.tuckey.web.filters.urlrewrite.UrlRewriteFilter"/>
    <property name="initParameters">
      <props>
        <prop key="confPath">/WEB-INF/config/urlrewrite.xml</prop>
        <prop key="confReloadCheckInterval">10</prop>
        <prop key="statusEnabled">true</prop>
        <prop key="statusPath">/admin/rewrite-status</prop>
      </props>
    </property>
  </bean>

Note that I found and reported (and locally fixed) a bug in UrlRewriteFilter where changes to a config file in a non-standard location do not get picked up.

Posted by Alon Salant at 11:52 AM | Comments (0)