Sunday, April 3, 2011

Build Open Toast Web Site with Spring Roo in 10 Minutes

Two weeks ago, I started to learn Spring Roo by its reference document and tutorials. Now it is time to do some real exercises.

After reading Ben Alex's blog about his Wedding RSVP Web site, I want to do a similar Web site for Toastmasters.

The main goal for this exercise is to register our club members' personal data and confirm their email address. The process is very simple: admin user can login and register new member; a confirmation email will be sent out to new member's email address; new member clicks the hyperlink in the email to confirm the registration.

I opened STS 2.6.0 with Spring Roo 1.1.2, and followed Ben's tutorial. After a while I got this Roo script to set up my project:

project --topLevelPackage com.opentoast --projectName OpenToast --java 6
persistence setup --provider HIBERNATE --database HYPERSONIC_IN_MEMORY

entity --class ~.domain.Toastmaster --testAutomatically 
field string --fieldName firstName --sizeMin 2 --sizeMax 30
field string --fieldName lastName --notNull --sizeMin 2 --sizeMax 30
field string --fieldName email --sizeMax 30 --sizeMin 6
field string --fieldName phone --sizeMax 20
field boolean --fieldName confirmed

entity --class ~.domain.EmailConfirmation
field string --fieldName code --notNull 
field string --fieldName email --notNull
field date --fieldName confirmDate --type java.util.Date
field reference --fieldName toastmaster --type ~.domain.Toastmaster --notNull 
finder add --finderName findEmailConfirmationsByCodeEquals

interface --class ~.domain.ConfirmationService
class --class ~.domain.impl.ConfirmationServiceImpl

controller scaffold --entity ~.domain.Toastmaster --class ~.web.ToastmasterController
controller class --class ~.web.ConfirmationController

selenium test --controller ~.web.ToastmasterController

logging setup --package WEB --level DEBUG

security setup

email sender setup --hostServer smtp.gmail.com --protocol SMTP --port 587 --username youraccount --password yourpassword
field email template --class ~.domain.impl.ConfirmationServiceImpl

perform eclipase

The domain model only contains two entity, Toastmaster and EmailConfirmation. (I cannot use Member or User, since those are reserved SQL keyword.)

One Web controller for maintaining Toastmaster records is genereated by controller scafford command. Spring Roo will generate all CRUD jsp pages.

Another controller for new member to confirm email address made by: controller class --class ~.web.ConfirmationController.

When admin user creates a new Toastmaster, a confirmation email will be sent out, and I want this logic to be in a service, so one service interface and one implementation class are created. But Spring Roo cannot generate real business logic code for us, so I have to add code manually to interface and implementation classes.

There are other changes in this application to get it working as we want. Because the last step in Roo script is perform eclipse, so just run this script in Roo and import the result project to STS, you have a running Web application ready to play with.

So first let's add logic to ConfirmationService:

src/main/java/com/opentoast/domain/ConfirmationService.java

package com.opentoast.domain;

public interface ConfirmationService {
    void sendConfirmationEmail(Toastmaster toastmaster);
}

src/main/java/com/opentoast/domain/impl/ConfirmationServiceImpl.java

package com.opentoast.domain.impl;

import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailSender;
import org.springframework.stereotype.Component;

import com.opentoast.domain.ConfirmationService;
import com.opentoast.domain.EmailConfirmation;
import com.opentoast.domain.Toastmaster;

@Component("confirmationService")
public class ConfirmationServiceImpl implements ConfirmationService{
    @Autowired
    private transient MailSender mailTemplate;

    public void sendMessage(String mailFrom, String subject, String mailTo, String message) {
        org.springframework.mail.SimpleMailMessage simpleMailMessage = new org.springframework.mail.SimpleMailMessage();
        simpleMailMessage.setFrom(mailFrom);
        simpleMailMessage.setSubject(subject);
        simpleMailMessage.setTo(mailTo);
        simpleMailMessage.setText(message);
        mailTemplate.send(simpleMailMessage);
    }

    @Override
    public void sendConfirmationEmail(Toastmaster toastmaster) {
        //create a new email confirmation record
        EmailConfirmation emailConfirmation = new EmailConfirmation();
        emailConfirmation.setCode(UUID.randomUUID().toString());
        emailConfirmation.setEmail(toastmaster.getEmail());
        emailConfirmation.setToastmaster(toastmaster);
        emailConfirmation.persist();
        
        //build email content
        StringBuffer sb = new StringBuffer();
        sb.append("Please confirm your email address '");
        sb.append(toastmaster.getEmail());
        sb.append("' by clicking http://localhost:8080/OpenToast/confirmation/");
        sb.append(emailConfirmation.getCode());
        sb.append("\n Thank you!");
        sb.append("\n\nOpen Toast Project");
        
        sendMessage("Open Toast Project <opentoastproject@gmail.com>", "Please Confirm Your Email to Open Toast", toastmaster.getEmail(), sb.toString()); 
    }

}

The ConfirmationServiceImpl is a component (annotated by @Component), so we don't need to change any configuration, and it will be automatically handled by Spring container. Next, inject this component to ToastmasterController so when admin creates a new member, it will use this service to send out email. Add create method to ToastmasterController class, and Spring Roo will automatically remove same method in aspectj file ToastmasterController_Roo_Controller.aj.

src/main/java/com/opentoast/web/ToastmasterController.java

package com.opentoast.web;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.roo.addon.web.mvc.controller.RooWebScaffold;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.opentoast.domain.ConfirmationService;
import com.opentoast.domain.Toastmaster;

@RooWebScaffold(path = "toastmasters", formBackingObject = Toastmaster.class)
@RequestMapping("/toastmasters")
@Controller
public class ToastmasterController {

    @Autowired
    private transient ConfirmationService confirmationService;

    @RequestMapping(method = RequestMethod.POST)
    public String create(@Valid Toastmaster toastmaster, BindingResult bindingResult, Model uiModel, HttpServletRequest httpServletRequest) {
        if (bindingResult.hasErrors()) {
            uiModel.addAttribute("toastmaster", toastmaster);
            return "toastmasters/create";
        }
        uiModel.asMap().clear();
        toastmaster.persist();
        confirmationService.sendConfirmationEmail(toastmaster);
        
        return "redirect:/toastmasters/" + encodeUrlPathSegment(toastmaster.getId().toString(), httpServletRequest);
    }

}

Here we only add code to inject ConfirmationService object, and call its sendConfirmationEmail methos when a new Toastmaster record is created.

The email contains a URL, and when new member clicks this URL, it will send HTTP GET request with the confirmation code in it. Spring MVC can handle it very nicely with URL Template /confirmation/{code}, so the code value is bound to parameter String code. By using Roo auto generated finder method EmailConfirmation.findEmailConfirmationsByCodeEquals, we can get the EmailConfirmation object used for this confirmation, and set the confirmation date, set Toastmaster object as confirmed.

So in ConfirmationController class, remove all generated methods, and add confirmByCode method to do the logic:

src/main/java/com/opentoast/web/ConfirmationController.java

package com.opentoast.web;

import java.util.Date;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.opentoast.domain.EmailConfirmation;

@RequestMapping("/confirmation/**")
@Controller
public class ConfirmationController {
    @RequestMapping(value="/confirmation/{code}", method=RequestMethod.GET)
    public String confirmByCode(@PathVariable String code, ModelMap modelMap) {
        try {
            EmailConfirmation emailConfirmation = (EmailConfirmation)EmailConfirmation.findEmailConfirmationsByCodeEquals(code).getSingleResult();
            emailConfirmation.setConfirmDate(new Date());
            emailConfirmation.getToastmaster().setConfirmed(true);
            emailConfirmation.persist();
            
            modelMap.put("confirmation", emailConfirmation);
        } catch (Exception e) {
            // ignore
        }
        return "confirmation/thanks"; 
    }

}

After member clicks URL, we want to display a thank you message, so let's change the generated index.jspx name to thanks.jspx in confirmation folder, and add a spring message:

src/main/webapp/WEB-INF/views/confirmation/thanks.jspx

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:spring="http://www.springframework.org/tags" xmlns:util="urn:jsptagdir:/WEB-INF/tags/util" version="2.0">
  <jsp:directive.page contentType="text/html;charset=UTF-8"/>
  <jsp:output omit-xml-declaration="yes"/>
  <spring:message code="label_confirmation_index" htmlEscape="false" var="title"/>
  <util:panel id="title" title="${title}">
    <spring:message code="application_name" htmlEscape="false" var="app_name"/>
    <h3>
      <spring:message arguments="${app_name}" code="welcome_titlepane"/>
    </h3>
    <p>
      <spring:message code="thanks_text" arguments="${confirmation.email}"/>
    </p>
  </util:panel>
</div>

In order to display the message, we add the text to messages.properties:

src/main/webapp/WEB-INF/i18n/messages.properties

thanks_text=You have successfully confirmed your email {0}. Thank you for signing up Open Toast.

And update view.xml to reflect the name changes:

src/main/webapp/WEB-INF/views/confirmation/views.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN" "http://tiles.apache.org/dtds/tiles-config_2_1.dtd">
<tiles-definitions>
    <definition extends="public" name="confirmation/thanks">
        <put-attribute name="body" value="/WEB-INF/views/confirmation/thanks.jspx"/>
    </definition>
</tiles-definitions>
pr/> last change is to security setting. The whole Web site should only be accessed by authenticated user. We only allow admin user to maintain Toastmaster data in toastmaster path, but un-authenticated user can access confirmation path to confirm.

src/main/resources/META-INF/spring/applicationContext-security.xml

...
<!-- HTTP security configurations -->
    <http auto-config="true" use-expressions="true">
        <form-login login-processing-url="/resources/j_spring_security_check" login-page="/login" authentication-failure-url="/login?login_error=t"/>
        <logout logout-url="/resources/j_spring_security_logout"/>
        
        <!-- Configure these elements to secure URIs in your application -->
        <intercept-url pattern="/toastmasters/**" access="hasRole('ROLE_ADMIN')"/>
        <intercept-url pattern="/confirmation/**" access="permitAll" />
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/login**" access="permitAll" />
<intercept-url pattern="/**" access="isAuthenticated()" />
    </http>
...

If you didn't change gmail account ID and password, you can open email.properties file and update with your personal gmail account and password.

So that's it. You can test this Web application by running mvn tomcat:run in the directory you run the Roo script, and open http://localhost:8080/OpenToast. First login with admin account and add a new Toastmaster with a valid email address. Then check the email, and click the URL, you will see the thank you page. Go back to the Web site and list all Toastmasters, you will see the confirmed flag is true now.

2 comments:

jiwhiz said...

Today I got CloudFoundry account and tried to use STS to deploy this app. I could not open OpenToast home page, got infinite loop of redirect. Then I realized security config is wrong. I missed one line of setting to allow login "permitAll". Update the config and app is running in the cloud. Cool!

jiwhiz said...

The application in CloudFoundry cannot send emails. I posted a question in forum Cannot send email through Gmail, what's wrong?

The answer is "currently the only outbound connections allowed at this time are proxied HTTP and HTTPS." So I will either have to use thrid party email service through HTTP or wait for new service from CloudFoundry in the future.

I will just wait. :-)