Recently a bot figured out how to submit my comment form. It was pretty funny I had more than 700 comments telling me how interesting my post on how to install a jar in maven was. I decided to add captcha support and while I was at it, make the comment form use DWR for submission so that the page didn't refresh etc.
I have created a little example application demonstrating how I integrated JCaptcha with DWR. The example is also (hopefully) serves as an example of how to use the new Spring Namespace support that has been added to DWR. I think if you're using DWR with Spring 2 and you want to keep all your configuration within Spring, you should use the Namespace support for configuring DWR. It's just easier. Please note this example only runs in a Java 5 environment and that if you're using DWR 2.0.rc2 there is a small issue with using Spring 2.0.1 or better so I've had to use Spring 2.0. This issue is fixed in CVS HEAD so if you want to build DWR yourself from source you should have no problems with Spring 2.0.1 or 2.0.2.
DWR Configuration in Spring
To keep things simple, in this example all the DWR configuration is done in the spring-catpcha-controllers.xml spring configuration. First we register the DwrController using:
<dwr:controller id="dwrController" debug="false" />
Now we need to tell DWR what domain objects we'd like to expose using javascript. DWR does this using Converters. In this case I'm really only exposing the Comment domain object:
<dwr:convert type="bean" class="com.kuripai.example.domain.Comment">
<dwr:include method="id" />
<dwr:include method="title" />
<dwr:include method="postedBy" />
<dwr:include method="website" />
<dwr:include method="email" />
<dwr:include method="body" />
<dwr:include method="createdAt" />
<dwr:include method="humanResponse" />
</dwr:convert>
The next step is to configure and expose the service that saves comments. I've created a very simple WeblogService for this purpose which just saves the comments into a java.util.List. Obviously a production version would save the comments in a database or similar. Here is the configuration:
<bean id="weblogService" class="com.kuripai.example.service.WeblogService" >
<dwr:remote javascript="weblogService">
<!-- Methods that are allowed to be exposed via javascript -->
<dwr:include method="getComments" />
<dwr:include method="saveComment" />
<!-- Filter to handle Captcha -->
<dwr:filter class="com.kuripai.example.dwr.captcha.CaptchaAjaxFilter" />
</dwr:remote>
<property name="validators">
<list>
<ref local="captchaValidator" />
<bean class="com.kuripai.example.domain.CommentValidator" />
</list>
</property>
</bean>
As you can see you configure your service as normal with the additional <dwr:remote javascript="weblogService" /> element telling DWR what the name of the service should be in Javascript and the <dwr:include method="xxx" /> elements telling DWR what methods are allowed to be exposed. Also note that I've exposed the service directly via DWR. Personally I don't normally do this. Instead I write a very simple proxy around the service and expose that instead. One reason I like to do this is to invoke regular Spring Validators and return a regular org.springframework.validation.Errors object to DWR. I'm not really sure if this is such a great idea. Just something I'm playing with at the moment.
public Errors saveComment(Comment comment) {
Errors errors = new BindException(comment, "comment");
for (int i = 0; i < validators.length; i++) {
ValidationUtils.invokeValidator(validators[i], comment, errors);
}
if (!errors.hasErrors()) {
comments.add(comment);
comment.setId(Long.valueOf((long)comments.size()));
}
return errors;
}
The org.directwebremoting.AjaxFilter
You might have notices the <dwr:filter /> element in the previous configuration code snippet. DWR 2 introduced the org.directwebremoting.AjaxFilter that can be configured to be invoked per request for a method exposed by DWR. I use a filter to here to get the current Captcha ID in Session and put it in a threadlocal based holder object for use in the Captcha validator.
public Object doFilter(Object object, Method method, Object[] params, AjaxFilterChain chain) throws Exception {
if (logger.isDebugEnabled()) {
logger.info("Processing method '" + method.getName() + "' on service '" + object + "'");
}
HttpSession session = WebContextFactory.get().getSession();
CaptchaResponseHolder.setCaptchaId(session.getId());
Object reply = chain.doFilter(object, method, params);
return reply;
}
The CaptchaValidator then is a regular org.springframework.validation.Validator:
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, CAPTCHA_FIELD_NAME, "form.error.required",
new Object[] { new DefaultMessageSourceResolvable("form.captcha") }, "Please enter the validation word");
if (errors.getFieldError(CAPTCHA_FIELD_NAME) != null) {
return;
}
CaptchaAware captchaAware = (CaptchaAware)target;
if (!isCaptchaValid(captchaAware.getHumanResponse())) {
errors.rejectValue(CAPTCHA_FIELD_NAME, "form.error.captchaInvalid", "Please enter the security word again");
}
}
private boolean isCaptchaValid(Object response) {
String captchaId = CaptchaResponseHolder.getCaptchaId();
if (logger.isDebugEnabled()) {
logger.debug("Validating captcha response '" + response + "' for captchaId '" + captchaId + "'");
}
Boolean isValid = captchaService.validateResponseForID(captchaId, response);
return isValid.booleanValue();
}
Running the Example
That's pretty much it. I've zipped up the source code for the example here. Unzip this into a directory of your choice and type mvn tomcat:run after completing the steps below providing of course you have maven 2 installed. You should also be able to import the project into eclipse to take a look at it.
Installing JCaptcha and Other Required Libraries in your Maven Repository
Unfortunately there seems to be a problem with the version of JCaptcha in on ibiblio. So you will need download and install the jar in you maven repository yourself following these steps (assuming you have Maven 2 installed):
1. Download jcaptcha-1.0-RC3 binary from the projects download page.
2. Unzip the downloaded jcaptcha binary and cd into the directory where you unzipped it.
3. Install the jar using this command:
mvn install:install-file -DgroupId=jcaptcha -DartifactId=jcaptcha-all \
-Dversion=1.0-RC3 -Dpackaging=jar -Dfile=jcaptcha-all-1.0-RC3.jar
Running the Example
You should now be ready to run the example. From the command line, cd into the directory where you downloaded the example. Type
mvn clean tomcat:run
Maven should download a whole bunch of jar files including a enough to run an embedded version of Tomcat. Which will start up and run.
Navigate to http://localhost:8080/dwr-captcha-example/example/comments.html and you should see a regular looking comment form with captcha that never refreshes.
