Monday, February 28, 2011

Spring 3: REST Web Service Provider and Client Tutorial (Part 2)

In this tutorial we'll create a Spring MVC REST client that uses Spring 3's RestTemplate. Our client is able to communicate via XML and JSON. It also has basic support for retrieving images. This tutorial is Part 2 of our Spring 3: REST Web Service Provider and Client Tutorial series. If you haven't read Part 1, please read it first (See here).

What is REST in Spring 3?
For a great introduction, please see the following links:
- REST in Spring 3: @MVC
- REST in Spring 3: RestTemplate

What is RestTemplate?
The central class for client-side HTTP access. It simplifies communication with HTTP servers, and enforces RESTful principles. It handles HTTP connections, leaving application code to provide URLs (with possible template variables) and extract results.

Source: Spring 3 Reference: RestTemplate

Development

Our client application is unique because it doesn't need a Service layer. We only need the Domain and the Controller layers. We also need a couple of JSP pages to display the records.

Domain Layer

We'll be declaring two domain objects: Person and PersonList. These are the same class declarations we had in the REST provider application.

Person.java
package org.krams.tutorial.domain;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="person")
public class Person {

 private Long id;
 private String firstName;
 private String lastName;
 private Double money;
 
 public Long getId() {
  return id;
 }
 public void setId(Long id) {
  this.id = id;
 }
 public String getFirstName() {
  return firstName;
 }
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 public String getLastName() {
  return lastName;
 }
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 public Double getMoney() {
  return money;
 }
 public void setMoney(Double money) {
  this.money = money;
 }
}
Person is a simple POJO consisting of three properties. Notice we've annotated the class name with @XmlRootElement(name="person"). Its purpose is to help JAXB (the one responsible for marshalling/unmarshalling to XML) determine the root of our POJO.

PersonList.java
package org.krams.tutorial.domain;

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="persons")
public class PersonList {

 @XmlElement(required = true)
 public List<person> data;

 @XmlElement(required = false)
 public List<person> getData() {
  return data;
 }

 public void setData(List<person> data) {
  this.data = data;
 }
}
PersonList is another simple POJO. It's purpose is to wrap a list of Person objects.

Ideally we should be able to return a List instead of a PersonList. However, JAXB has difficulties processing java.util.List. A simple Google search verifies this unwanted behavior, for example, http://stackoverflow.com/questions/298733/java-util-list-is-an-interface-and-jaxb-cant-handle-interfaces

To resolve this issue with JAXB, we wrap our List with another object. To learn more about JAXB please visit the following link: http://download.oracle.com/javaee/5/tutorial/doc/bnazf.html

Controller Layer

The controller layer is the most important section of the client application because here's where we communicate with the REST provider. It's also responsible for displaying the JSP pages.

ProviderController.java
package org.krams.tutorial.controller;

import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.krams.tutorial.domain.PersonList;
import org.krams.tutorial.util.Writer;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletResponse;

/**
 * REST service client
 */
@Controller
public class RestClientController {

 protected static Logger logger = Logger.getLogger("controller");
 
 private RestTemplate restTemplate = new RestTemplate();
 
 /**
  * Retrieves an image from the REST provider
  * and writes the response to an output stream.
  */
 @RequestMapping(value = "/getphoto", method = RequestMethod.GET)
 public void getPhoto(@RequestParam("id") Long id, HttpServletResponse response) {
  logger.debug("Retrieve photo with id: " + id);
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.IMAGE_JPEG);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<String> entity = new HttpEntity<String>(headers); 
  
  // Send the request as GET
  ResponseEntity<byte[]> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.GET, entity, byte[].class, id);
  
  // Display the image
  Writer.write(response, result.getBody());
 }
 
 /**
  * Retrieves all records from the REST provider
  * and displays the records in a JSP page
  */
 @RequestMapping(value = "/getall", method = RequestMethod.GET)
 public String getAll(Model model) {
  logger.debug("Retrieve all persons");
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<Person> entity = new HttpEntity<Person>(headers);

  // Send the request as GET
  try {
   ResponseEntity<PersonList> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/persons", HttpMethod.GET, entity, PersonList.class);
   // Add to model
   model.addAttribute("persons", result.getBody().getData());
   
  } catch (Exception e) {
   logger.error(e);
  }
  
  // This will resolve to /WEB-INF/jsp/personspage.jsp
  return "personspage";
 }
 
 /**
  * Retrieves a single record from the REST provider
  * and displays the result in a JSP page
  */
 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public String getPerson(@RequestParam("id") Long id, Model model) {
  logger.debug("Retrieve person with id: " + id);
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<Person> entity = new HttpEntity<Person>(headers);

  // Send the request as GET
  try {
   ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.GET, entity, Person.class, id);
   // Add to model
   model.addAttribute("person", result.getBody());
   
  } catch (Exception e) {
   logger.error(e);
  }
  
  // This will resolve to /WEB-INF/jsp/getpage.jsp
  return "getpage";
 }
 
 /**
     * Retrieves the JSP add page
     */
 @RequestMapping(value = "/add", method = RequestMethod.GET)
 public String getAddPage(Model model) {
  logger.debug("Received request to show add page");

  // Create new Person and add to model
  // This is the formBackingOBject
  model.addAttribute("personAttribute", new Person());

  // This will resolve to /WEB-INF/jsp/addpage.jsp
  return "addpage";
 }
 
 /**
  * Sends a new record to the REST provider
  * based on the information submitted from the JSP add page.
  */
 @RequestMapping(value = "/add", method = RequestMethod.POST)
 public String addPerson(@ModelAttribute("personAttribute") Person person,
       Model model) {
  logger.debug("Add new person");
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  // Pass the new person and header
  HttpEntity<Person> entity = new HttpEntity<Person>(person, headers);

  // Send the request as POST
  try {
   ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person", HttpMethod.POST, entity, Person.class);
  } catch (Exception e) {
   logger.error(e);
  }
  
  // This will resolve to /WEB-INF/jsp/resultpage.jsp
  return "resultpage";
 }
 
 /**
     * Retrieves the JSP update page
     */
    @RequestMapping(value = "/update", method = RequestMethod.GET)
    public String getUpdatePage(@RequestParam(value="id", required=true) Integer id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing Person and add to model
     // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<Person> entity = new HttpEntity<Person>(headers);

  // Send the request as GET
  try {
   ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.GET, entity, Person.class, id);
   // Add to model
   model.addAttribute("personAttribute", result.getBody());
   
  } catch (Exception e) {
   logger.error(e);
  }
     
     // This will resolve to /WEB-INF/jsp/updatepage.jsp
     return "updatepage";
 }
 
 /**
  * Sends an update request to the REST provider
  * based on the information submitted from the JSP update page.
  */
    @RequestMapping(value = "/update", method = RequestMethod.POST)
 public String updatePerson(@ModelAttribute("personAttribute") Person person,
      @RequestParam(value="id",  required=true) Long id,
      Model model) {
  logger.debug("Update existing person");
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  // Pass the new person and header
  HttpEntity<Person> entity = new HttpEntity<Person>(person, headers);
  
  // Send the request as PUT
  ResponseEntity<String> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.PUT, entity, String.class, id);

  // This will resolve to /WEB-INF/jsp/resultpage.jsp
  return "resultpage";
    }
    
    /**
  * Sends a delete request to the REST provider
  */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
 public String deletePerson(@RequestParam("id") Long id,
          Model model) {
     logger.debug("Delete existing person");
     
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
     HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<String> entity = new HttpEntity<String>(headers); 
  
  // Send the request as DELETE
  ResponseEntity<String> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.DELETE, entity, String.class, id);
  
  // This will resolve to /WEB-INF/jsp/resultpage.jsp
  return "resultpage";
    }
}
Notice our controller is our typical Spring MVC 3 controller.

Let's dissect our controller and analyze each methods.

getPhoto() method

The getPhoto() method retrieves an image as a byte array from the REST provider. It displays the image by writing it to the output stream using a helper class: Writer
/**
  * Retrieves an image from the REST provider
  * and writes the response to an output stream.
  */
 @RequestMapping(value = "/getphoto", method = RequestMethod.GET)
 public void getPhoto(@RequestParam("id") Long id, HttpServletResponse response) {
  logger.debug("Retrieve photo with id: " + id);
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.IMAGE_JPEG);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<String> entity = new HttpEntity<String>(headers); 
  
  // Send the request as GET
  ResponseEntity<byte[]> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.GET, entity, byte[].class, id);
  
  // Display the image
  Writer.write(response, result.getBody());
 }

Writer.java
package org.krams.tutorial.util;

import java.io.ByteArrayOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;

/**
 * Writes the report to the output stream
 * 
 * @author Krams at {@link http://krams915@blogspot.com}
 */
public class Writer {

 private static Logger logger = Logger.getLogger("service");
 /**
  * Writes the report to the output stream
  */
 public static void write(HttpServletResponse response, ByteArrayOutputStream bao) {
  
  logger.debug("Writing report to the stream");
  try {
   // Retrieve the output stream
   ServletOutputStream outputStream = response.getOutputStream();
   // Write to the output stream
   bao.writeTo(outputStream);
   // Flush the stream
   outputStream.flush();
   // Close the stream
   outputStream.close();

  } catch (Exception e) {
   logger.error("Unable to write report to the output stream");
  }
 }
 
 /**
  * Writes the report to the output stream
  */
 public static void write(HttpServletResponse response, byte[] byteArray) {
  
  logger.debug("Writing report to the stream");
  try {
   // Retrieve the output stream
   ServletOutputStream outputStream = response.getOutputStream();
   // Write to the output stream
   outputStream.write(byteArray);
   // Flush the stream
   outputStream.flush();
   // Close the stream
   outputStream.close();
   
  } catch (Exception e) {
   logger.error("Unable to write report to the output stream");
  }
 } 
}

To call this method, we have to use the following URL:
http://localhost:8081/spring-rest-client/krams/getphoto?id=1
Notice the port number is 8081. That's because I'm using two versions of Tomcat. For the REST provider, I use port 8080. For the REST client, I use port 8081. Depending on how you setup your server, please modify the URL accordingly.

Running the URL displays the following image:

I think you know who this person is (and it just so happen this is the photo I have in my desktop). Notice whatever id you use the application will show the same image. That's because the provider uses a fix image from the classpath. Anyway the point of this exercise is how to retrieve a byte array using RestTemplate.

getAll() method

The getAll() method sends a request to retrieve all records from the REST provider. Once the result has arrived, it is added to the model to be displayed in a JSP page.

/**
  * Retrieves all records from the REST provider
  * and displays the records in a JSP page
  */
 @RequestMapping(value = "/getall", method = RequestMethod.GET)
 public String getAll(Model model) {
  logger.debug("Retrieve all persons");
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<Person> entity = new HttpEntity<Person>(headers);

  // Send the request as GET
  try {
   ResponseEntity<PersonList> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/persons", HttpMethod.GET, entity, PersonList.class);
   // Add to model
   model.addAttribute("persons", result.getBody().getData());
   
  } catch (Exception e) {
   logger.error(e);
  }
  
  // This will resolve to /WEB-INF/jsp/personspage.jsp
  return "personspage";
 }

To call this method, we have to use the following URL:
http://localhost:8081/spring-rest-client/krams/getall

Running the URL shows the following page:

getPerson() method

The getPerson() method retrieves a single record based on the id from the REST provider. Once the result has arrived, it is added to the model to be displayed in a JSP page.

/**
  * Retrieves a single record from the REST provider
  * and displays the result in a JSP page
  */
 @RequestMapping(value = "/get", method = RequestMethod.GET)
 public String getPerson(@RequestParam("id") Long id, Model model) {
  logger.debug("Retrieve person with id: " + id);
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<Person> entity = new HttpEntity<Person>(headers);

  // Send the request as GET
  try {
   ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.GET, entity, Person.class, id);
   // Add to model
   model.addAttribute("person", result.getBody());
   
  } catch (Exception e) {
   logger.error(e);
  }
  
  // This will resolve to /WEB-INF/jsp/getpage.jsp
  return "getpage";
 }

To call this method, we have to use the following URL:
http://localhost:8081/spring-rest-client/krams/get?id=1
Alternatively we can click the Get link from the getAll page.

Running the URL shows the following page:

getAddPage() and addPerson() methods

The getAddPage() method returns a JSP form where the user can add a new record.

     /**
     * Retrieves the JSP add page
     */
 @RequestMapping(value = "/add", method = RequestMethod.GET)
 public String getAddPage(Model model) {
  logger.debug("Received request to show add page");

  // Create new Person and add to model
  // This is the formBackingOBject
  model.addAttribute("personAttribute", new Person());

  // This will resolve to /WEB-INF/jsp/addpage.jsp
  return "addpage";
 }

To call this method, we have to use the following URL:
http://localhost:8081/spring-rest-client/krams/add?id=1
Alternatively we can click the Add link from the getAll page

Running the URL shows the following page:

When the form is submitted, the addPerson() method takes control. Its purpose is to send a new record by sending a POST request to the REST provider.

/**
  * Sends a new record to the REST provider
  * based on the information submitted from the JSP add page.
  */
 @RequestMapping(value = "/add", method = RequestMethod.POST)
 public String addPerson(@ModelAttribute("personAttribute") Person person,
       Model model) {
  logger.debug("Add new person");
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  // Pass the new person and header
  HttpEntity<Person> entity = new HttpEntity<Person>(person, headers);

  // Send the request as POST
  try {
   ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person", HttpMethod.POST, entity, Person.class);
  } catch (Exception e) {
   logger.error(e);
  }
  
  // This will resolve to /WEB-INF/jsp/resultpage.jsp
  return "resultpage";
 }
To see the new record, please refresh the getAll page.

getUpdatePage() and updatePerson() methods

The getUpdatePage() method returns a JSP form where the user can edit an existing record.

 /**
     * Retrieves the JSP update page
     */
    @RequestMapping(value = "/update", method = RequestMethod.GET)
    public String getUpdatePage(@RequestParam(value="id", required=true) Integer id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing Person and add to model
     // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<Person> entity = new HttpEntity<Person>(headers);

  // Send the request as GET
  try {
   ResponseEntity<Person> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.GET, entity, Person.class, id);
   // Add to model
   model.addAttribute("personAttribute", result.getBody());
   
  } catch (Exception e) {
   logger.error(e);
  }
     
     // This will resolve to /WEB-INF/jsp/updatepage.jsp
     return "updatepage";
 }

To call this method, we have to use the following URL:
http://localhost:8081/spring-rest-client/krams/update?id=1
Alternatively we can click the Edit link from the getAll page

Running the URL shows the following page:

When the form is submitted, the updatePerson() method takes control. Its purpose is to send an updated record by sending a PUT request to the REST provider.

/**
  * Sends an update request to the REST provider
  * based on the information submitted from the JSP update page.
  */
    @RequestMapping(value = "/update", method = RequestMethod.POST)
 public String updatePerson(@ModelAttribute("personAttribute") Person person,
      @RequestParam(value="id",  required=true) Long id,
      Model model) {
  logger.debug("Update existing person");
  
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  // Pass the new person and header
  HttpEntity<Person> entity = new HttpEntity<Person>(person, headers);
  
  // Send the request as PUT
  ResponseEntity<String> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.PUT, entity, String.class, id);

  // This will resolve to /WEB-INF/jsp/resultpage.jsp
  return "resultpage";
    }
To see updated record, please refresh the getAll page.

deletePerson() method

The deletePerson() method sends a DELETE request to delete an existing record based on the submitted id.

    /**
  * Sends a delete request to the REST provider
  */
    @RequestMapping(value = "/delete", method = RequestMethod.GET)
 public String deletePerson(@RequestParam("id") Long id,
          Model model) {
     logger.debug("Delete existing person");
     
  // Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
     HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  HttpEntity<String> entity = new HttpEntity<String>(headers); 
  
  // Send the request as DELETE
  ResponseEntity<String> result = restTemplate.exchange("http://localhost:8080/spring-rest-provider/krams/person/{id}", HttpMethod.DELETE, entity, String.class, id);
  
  // This will resolve to /WEB-INF/jsp/resultpage.jsp
  return "resultpage";
    }

To call this method, we have to use the following URL:
http://localhost:8081/spring-rest-client/krams/delete?id=1
Alternatively we can click the Delete link from the getAll page

When you run the URL it will automatically send a request. To see the results, please refresh the getAll page.

Configuration

The application's configuration is quite trivial. It's just a basic Spring MVC configuration. To spare some space, I will not post the XML configurations here. Instead, you can look at them by checking the source code at the end of this tutorial.

Considerations

Notice in the header of each request we set the media type to APPLICATION_XML. We can equally change it to APPLICATION_JSON to accept JSON objects.

APPLICATION_XML
// Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_XML);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  // Pass the new person and header
  HttpEntity<Person> entity = new HttpEntity<Person>(person, headers);

APPLICATION_JSON
// Prepare acceptable media type
  List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
  acceptableMediaTypes.add(MediaType.APPLICATION_JSON);
  
  // Prepare header
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(acceptableMediaTypes);
  // Pass the new person and header
  HttpEntity<Person> entity = new HttpEntity<Person>(person, headers);

For JSON to work, you need to have the Jackson library in your classpath. Make sure to check the pom.xml!

Conclusion

That's it. We've managed to create a REST client using Spring 3's RestTemplate. We've exchanged information with the provider via XML and displayed the result in a JSP page using Spring MVC.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-rest-guide/

You can download the project as a Maven build. Look for the spring-rest-client.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring 3: REST Web Service Provider and Client Tutorial (Part 2) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Saturday, February 26, 2011

Spring 3: REST Web Service Provider and Client Tutorial (Part 1)

In this tutorial we'll create a RESTful web service based on Spring 3. We'll be developing a REST provider using URI templates that returns XML and JSON representations of our data. We'll also add basic support for retrieving images and html pages. We'll test our provider via RESTClient to verify the service's availability. In part 2, we'll add an actual client based on RestTemplate and Spring MVC that will access our service.

What is REST?
Representational State Transfer (REST) is a style of software architecture for distributed hypermedia systems such as the World Wide Web. The term Representational State Transfer was introduced and defined in 2000 by Roy Fielding in his doctoral dissertation.

Source: http://en.wikipedia.org/wiki/Representational_State_Transfer

The Representational State Transfer (REST) style is an abstraction of the architectural elements within a distributed hypermedia system. REST ignores the details of component implementation and protocol syntax in order to focus on the roles of components, the constraints upon their interaction with other components, and their interpretation of significant data elements. It encompasses the fundamental constraints upon components, connectors, and data that define the basis of the Web architecture, and thus the essence of its behavior as a network-based application.

Source: Architectural Styles and the Design of Network-based Software Architectures, a dissertation of Roy Thomas Fielding,

REST vs SOAP
Unlike SOAP-based web services, there is no "official" standard for RESTful web services. This is because REST is an architecture, unlike SOAP, which is a protocol. Even though REST is not a standard, a RESTful implementation such as the Web can use standards like HTTP, URL, XML, PNG, etc.

Source: http://en.wikipedia.org/wiki/Representational_State_Transfer

What is REST in Spring 3?
For a great introduction, please see the following links:
- REST in Spring 3: @MVC
- REST in Spring 3: RestTemplate

The Provider

Our application is a simple CRUD system for managing a list of persons. We'll start our project with the provider service which produces XML and JSON representations of our data.

The Domain Layer

We'll be declaring two domain objects: Person and PersonList

Person.java
package org.krams.tutorial.domain;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="person")
public class Person {

 private Long id;
 private String firstName;
 private String lastName;
 private Double money;
 
 public Long getId() {
  return id;
 }
 public void setId(Long id) {
  this.id = id;
 }
 public String getFirstName() {
  return firstName;
 }
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 public String getLastName() {
  return lastName;
 }
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }
 public Double getMoney() {
  return money;
 }
 public void setMoney(Double money) {
  this.money = money;
 }
}
Person is a simple POJO consisting of three properties. Notice we've annotated the class name with @XmlRootElement(name="person"). Its purpose is to help JAXB (the one responsible for marshalling/unmarshalling to XML) determine the root of our POJO.

PersonList.java
package org.krams.tutorial.domain;

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="persons")
public class PersonList {

 @XmlElement(required = true)
 public List<person> data;

 @XmlElement(required = false)
 public List<person> getData() {
  return data;
 }

 public void setData(List<person> data) {
  this.data = data;
 }
}
PersonList is another simple POJO. It's purpose is to wrap a list of Person objects.

Ideally we should be able to return a List instead of a PersonList. However, JAXB has difficulties processing java.util.List. A simple Google search verifies this unwanted behavior, for example, http://stackoverflow.com/questions/298733/java-util-list-is-an-interface-and-jaxb-cant-handle-interfaces

To resolve this issue with JAXB, we wrap our List with another object. To learn more about JAXB please visit the following link: http://download.oracle.com/javaee/5/tutorial/doc/bnazf.html

The Controller Layer

The controller layer is the most important section of the provider service because here's where we define the RESTful services available to our clients.

ProviderController.java
package org.krams.tutorial.controller;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import javax.imageio.ImageIO;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.krams.tutorial.domain.PersonList;
import org.krams.tutorial.service.PersonService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;

/**
 * REST service provider
 * 
 * Only GET and POST will return values
 * PUT and DELETE will not.
 */
@Controller
@RequestMapping("/provider")
public class RestProviderController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="personService")
 private PersonService personService;
 
 @RequestMapping(value = "/person/{id}", 
        method = RequestMethod.GET,
        headers="Accept=image/jpeg, image/jpg, image/png, image/gif")
 public @ResponseBody byte[] getPhoto(@PathVariable("id") Long id) {
  
  // Call service here
  try {
   // Retrieve image from the classpath
   InputStream is = this.getClass().getResourceAsStream("/bella.jpg"); 
   
   // Prepare buffered image
   BufferedImage img = ImageIO.read(is);
   
   // Create a byte array output stream
   ByteArrayOutputStream bao = new ByteArrayOutputStream();
   
   // Write to output stream
   ImageIO.write(img, "jpg", bao);
   
   logger.debug("Retrieving photo as byte array image");
   return bao.toByteArray();
   
  } catch (IOException e) {
   logger.error(e);
   throw new RuntimeException(e);
  }
 }
    
    @RequestMapping(value = "/person/{id}", 
        method = RequestMethod.GET, 
        headers="Accept=application/html, application/xhtml+xml")
 public String getPersonHTML(@PathVariable("id") Long id, Model model) {
     logger.debug("Provider has received request to get person with id: " + id);
  
  // Call service to here
  model.addAttribute("person",personService.get(id));
  
  return "getpage";
 }
    
 @RequestMapping(value = "/persons", 
        method = RequestMethod.GET, 
        headers="Accept=application/xml, application/json")
 public @ResponseBody PersonList getPerson() {
  logger.debug("Provider has received request to get all persons");
  
  // Call service here
  PersonList result = new PersonList();
  result.setData(personService.getAll());
  
  return result;
 }
 
    @RequestMapping(value = "/person/{id}", 
           method = RequestMethod.GET, 
           headers="Accept=application/xml, application/json")
 public @ResponseBody Person getPerson(@PathVariable("id") Long id) {
     logger.debug("Provider has received request to get person with id: " + id);
     
     // Call service here
  return personService.get(id);
    }
    
    @RequestMapping(value = "/person", 
           method = RequestMethod.POST, 
           headers="Accept=application/xml, application/json")
 public @ResponseBody Person addPerson(@RequestBody Person person) {
     logger.debug("Provider has received request to add new person");
     
     // Call service to here
     return personService.add(person);
    }
    
    
    @RequestMapping(value = "/person/{id}", 
           method = RequestMethod.PUT, 
           headers="Accept=application/xml, application/json")
 public @ResponseBody String updatePerson(@RequestBody Person person) {
     logger.debug("Provider has received request to edit person with id: " + id);
     
     // Call service here
     person.setId(id);
     return personService.edit(person).toString();
    }
    
    
    @RequestMapping(value = "/person/{id}", 
           method = RequestMethod.DELETE,
           headers="Accept=application/xml, application/json")
 public @ResponseBody String deleteEmployee(@PathVariable("id") Long id) {
     logger.debug("Provider has received request to delete person with id: " + id);
 
     // Call service here
     return personService.delete(id).toString();
    }
}

The URI Templates
Our controller has seven methods available. The first four are GET methods while the last three are POST, PUT, and DELETE methods.

The four GET methods correspond to the following:

1. Retrieving a single image based on a specific id
URL: http://localhost:8080/spring-rest-provider/krams/person/{id} 
Method: GET
Accept Header: image/jpeg, image/jpg, image/png, image/gif

2. Retrieving an HTML page based on a specific id
URL: http://localhost:8080/spring-rest-provider/krams/person/{id}
Method: GET
Accept Header: application/html, application/xhtml+xml

3. Retrieving an XML or JSON containing a list of persons
URL: http://localhost:8080/spring-rest-provider/krams/person
Method: GET
Accept Header: application/xml, application/json

4. Retrieving an XML or JSON containing a single person
URL: http://localhost:8080/spring-rest-provider/krams/person/{id}
Method: GET
Accept Header: application/xml, application/json

The last three methods correspond to the remaining CRUD services:

5. Adding a new person via XML and JSON
URL: http://localhost:8080/spring-rest-provider/krams/person
Method: POST
Accept Header: application/xml, application/json

6. Editing an existing person via XML and JSON
URL: http://localhost:8080/spring-rest-provider/krams/person/{id}
Method: PUT
Accept Header: application/xml, application/json

7. Deleting an existing person via its id
URL: http://localhost:8080/spring-rest-provider/krams/person/{id}
Method: DELETE
Accept Header: application/xml, application/json

HTTP Methods
Notice for most of the methods, we're just reusing the same URL http://localhost:8080/spring-rest-provider/krams/person and the only part that differs are the suffix {id} and the HTTP methods:

Method Purpose
GET Retrieves a representation of the requested resource
POST Creates a new representation of the requested resource
PUT Updates an existing representation of the requested resource
DELETE Deletes an existing representation of the requested resource

The Service Layer

The service layer is the one that does the actual processing of data. Here we're just managing an in-memory list to make the tutorial simpler to understand. Translating this to a real database isn't really that difficult

PersonService.java
package org.krams.tutorial.service;

import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;

@Service("personService")
public class PersonService {

 protected static Logger logger = Logger.getLogger("service");
 
 // In-memory list
 private List<Person> persons = new ArrayList<Person>();
 
 public PersonService() {
  logger.debug("Init database");
  
  // Create in-memory list
  Person person1 = new Person();
  person1.setId(0L);
  person1.setFirstName("John");
  person1.setLastName("Smith");
  person1.setMoney(1000.0);
  
  Person person2 = new Person();
  person2.setId(1L);
  person2.setFirstName("Jane");
  person2.setLastName("Adams");
  person2.setMoney(2000.0);
  
  Person person3 = new Person();
  person3.setId(2L);
  person3.setFirstName("Jeff");
  person3.setLastName("Mayer");
  person3.setMoney(3000.0);
  
  persons.add(person1);
  persons.add(person2);
  persons.add(person3);
 }
 
 /**
  * Retrieves all persons
  */
 public List<Person> getAll() {
  logger.debug("Retrieving all persons");
  
  return persons;
 }
 
 /**
  * Retrieves a single person
  */
 public Person get( Long id ) {
  logger.debug("Retrieving person with id: " + id);
  
  for (Person person:persons) {
   if (person.getId().longValue() == id.longValue()) {
    logger.debug("Found record");
    return person;
   }
  }
  
  logger.debug("No records found");
  return null;
 }
 
 /**
  * Adds a new person
  */
 public Person add(Person person) {
  logger.debug("Adding new person");
  
  try {
   // Find suitable id
   Long newId = 0L;
   Boolean hasFoundSuitableId = false;
   while(hasFoundSuitableId == false) {
    for (int i=0; i <persons.size(); i++) {
     if (persons.get(i).getId().longValue() == newId.longValue()) {
      newId++;
      break;
     }
     
     // Exit while loop
     if(i==persons.size()-1) {
      logger.debug("Assigning id: " + newId);
      hasFoundSuitableId = true;
     }
    }
   }
   
   // Assign new id
   person.setId(newId);
   // Add to list
   persons.add(person);
   
   logger.debug("Added new person");
   return person;
  } catch (Exception e) {
   logger.error(e);
   return null;
  }
 }
 
 /**
  * Deletes an existing person
  */
 public Boolean delete(Long id) {
  logger.debug("Deleting person with id: " + id);
  
  try {
   for (Person person:persons) {
    if (person.getId().longValue() == id.longValue()) {
     logger.debug("Found record");
     persons.remove(person);
     return true;
    }
   }
   
   logger.debug("No records found");
   return false;
   
  } catch (Exception e) {
   logger.error(e);
   return false;
  }
 }
 
 /**
  * Edits an existing person
  */
 public Boolean edit(Person person) {
  logger.debug("Editing person with id: " + person.getId());
  
  try {
   for (Person p:persons) {
    if (p.getId().longValue() == person.getId().longValue()) {
     logger.debug("Found record");
     persons.remove(p);
     persons.add(person);
     return true;
    }
   }
   
   logger.debug("No records found");
   return false;
   
  } catch (Exception e) {
   logger.error(e);
   return false;
  }
 }
}

Configuration

Configuring REST support in Spring isn't really different from configuring your usual Spring MVC. That's because REST in Spring are additional "features to Spring MVC itself" (See Rest in Spring 3: @MVC blog).

Here are all the XML configurations we're using for this project.

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
 
 <servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
 
 <servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/krams/*</url-pattern>
 </servlet-mapping>

 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
 
</web-app>

spring-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:p="http://www.springframework.org/schema/p" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 
 <!-- Declare a view resolver -->
 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" 
      p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />

</beans>

applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:mvc="http://www.springframework.org/schema/mvc"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context-3.0.xsd
   http://www.springframework.org/schema/mvc 
   http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
 
 <!-- Activates various annotations to be detected in bean classes -->
 <context:annotation-config />
 
 <!-- Scans the classpath for annotated components that will be auto-registered as Spring beans.
  For example @Controller and @Service. Make sure to set the correct base-package-->
 <context:component-scan base-package="org.krams.tutorial" />
 
 <!-- Configures the annotation-driven Spring MVC Controller programming model.
 Note that, with Spring 3.0, this tag works in Servlet MVC only!  -->
 <mvc:annotation-driven />

</beans>

Run the Application

To run the application, you'll need a client application, like a browser. Also you must access directly the URLs to reach a specific action.

Browser Issues

By default, you can only send a GET request in your browser without resorting to special plugins or tools. When you type a URL in the browser's address field, you're actually specifying a GET request. Using a browser we can only retrieve XML and HTML representations of our GET request (again without resorting to special plugins). Try it out and see what your browser will return.

On Chrome, typing the following URL
http://localhost:8080/spring-rest-provider/krams/person/1
yields an XML document:

On Firefox, typing the following URL
http://localhost:8080/spring-rest-provider/krams/person/1
yields an HTML document:

Testing with RESTClient

Using a browser we can only test the GET portion of the REST service. To fully test all methods, either we write a client application or reuse an existing client. Luckily for us there's a great Java client: RESTClient

What is RESTClient?
RESTClient is a Java application to test RESTful webservices. It can be used to test variety of HTTP communications. From version 2.3 Beta 1, it has two executable Jars:

- GUI version (restclient-ui-X.jar download)
- Cli version for batch execution of .rcq files (restclient-cli-X.jar download)

Source: http://code.google.com/p/rest-client/

Let's start by running RESTClient

Retrieve a single record (GET method)

1. To retrieve a record, select the GET method:

2. Click the Headers tab and enter the following information:
Key:  Accept
Value: application/xml or application/json


Make sure to hit the plus button to enter the values:

3. Enter the following URL:
http://localhost:8080/spring-rest-provider/krams/person/1
You should see the following results depending on the Accept header you provided earlier:

XML result

JSON result

Add a new record (POST method)

1. Under the Method tab, select the POST method.

2. Under the Headers tab, enter the following information:
Key:  Accept
Value: application/xml or application/json

3. Under the body tab, enter the following content (depending on the value field in step #2)
XML:
<person>
    <firstName>Roy</firstName>
    <lastName>Jones</lastName>
    <money>5000</money>
</person>
JSON:
{
  "firstName":"Roy"
  "lastName":"Jones"
  "money":5000
}

4. Enter the following URL:
http://localhost:8080/spring-rest-provider/krams/person
You should see the following results depending on the Accept header you provided earlier:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<person>
    <firstName>Roy</firstName>
    <lastName>Jones</lastName> 
    <money>5000.0</money>
</person>
JSON:
{
  "firstName" : "Roy",
  "lastName" : "Jones",
  "money" : 5000.0
}

Update an existing record (PUT method)

1. Under the Method tab, select the PUT method.

2. Under the Headers tab, enter the following information:
Key:  Accept
Value: application/xml or application/json

3. Under the body tab, enter the following content (depending on the value field in step #2)
XML:
<person>
    <firstName>Roy</firstName>
    <lastName>Jones</lastName>
    <money>5000</money>
</person>
JSON:
{
  "firstName":"Roy",
  "lastName":"Jones",
  "money":5000
}

4. Enter the following URL:
http://localhost:8080/spring-rest-provider/krams/person
You should see the following results depending on the Accept header you provided earlier:
XML:
true
JSON:
true
This means we've successfully updated record #1 with the new information we've provided. To verify if the record has been updated, create a GET request with the following URL:
http://localhost:8080/spring-rest-provider/krams/provider/person/1
Notice the number 1 at the end. That's the same number we used in the PUT method. You should see the updated record.

Delete an existing record (DELETE method)

1. Under the Method tab, select the DELETE method.

2. Under the Headers tab, enter the following information:
Key:  Accept
Value: application/xml or application/json

3. Enter the following URL:
http://localhost:8080/spring-rest-provider/krams/person/1
You should see the following results depending on the Accept header you provided earlier:
XML:
true
JSON:
true
This means we've successfully deleted record #1. To verify if the record has been deleted, create a GET request with the following URL:
http://localhost:8080/spring-rest-provider/krams/provider/person/1

Notice the number 1 at the end. That's the same number we used in the DELETE method. You should see an empty result.

Conclusion

That's it. We've managed to create a REST service using Spring 3. We've also shown how we can retrieve different representations of our resource by using a unified URL with varying HTTP methods. We've also tested the application using RESTClient. In Part 2 of the tutorial, we'll explore how to develop a client application using Spring's RestTemplate support.

To see Part 2 of this tutorial, click the following link: Spring 3: REST Web Service Provider and Client Tutorial (Part 2)

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-rest-guide/

You can download the project as a Maven build. Look for the spring-rest-provider.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring 3: REST Web Service Provider and Client Tutorial (Part 1) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Tuesday, February 22, 2011

Spring Security 3: Integrating reCAPTCHA Service

Introduction

In this tutorial we'll integrate Spring Security 3 with the popular reCAPTCHA service. For the Spring Security module we'll based the application from the one we built for Spring Security 3 - MVC: Using a Simple User-Service Tutorial. For the reCAPTCHA module we'll based the implementation from the Using reCAPTCHA with Java/JSP article. Our design goal here is to integrate the reCAPTCHA service unobtrusively. To realize that we'll be relying on Spring Security filters.

Here's what we will do:
1. Declare two CAPTCHA filters
2. Modify a JSP file to include reCAPTCHA login
3. Add two filters in the spring-security.xml configuration

Requirements:
1. A basic understanding of how to setup a simple Spring Security 3 application (See here)
2. A basic understanding of how to setup reCAPTCHA with JSP (See here)
3. An account with the reCAPTCHA service (See here). For the account, make sure you assigned the host that matches your web application. For example http://localhost.

What is reCAPTCHA?
reCAPTCHA is a free CAPTCHA service that helps to digitize books, newspapers and old time radio shows....

A CAPTCHA is a program that can tell whether its user is a human or a computer. You've probably seen them รณ colorful images with distorted text at the bottom of Web registration forms. CAPTCHAs are used by many websites to prevent abuse from "bots," or automated programs usually written to generate spam. No computer program can read distorted text as well as humans can, so bots cannot navigate sites protected by CAPTCHAs.

About 200 million CAPTCHAs are solved by humans around the world every day. In each case, roughly ten seconds of human time are being spent. Individually, that's not a lot of time, but in aggregate these little puzzles consume more than 150,000 hours of work each day. What if we could make positive use of this human effort? reCAPTCHA does exactly that by channeling the effort spent solving CAPTCHAs online into "reading" books.

Source: http://www.google.com/recaptcha/learnmore
Here's a screenshot of the reCAPTCHA service:

Architecture

Scenario 1: Spring Security
A form-based login has two input fields: username and password. To authenticate a user, the user must enter valid information. After submitting the values, the web application will query the database if the submitted values are present. The web application performs the validation.

Scenario 2: reCAPTCHA
A reCAPTCHA form has one input field. To authenticate a user, the user must match the text images shown on the form. After submitting the values, the web application will send a POST request to the reCAPTCHA service. The service performs the validation and returns the result as a booblean value. The web application can use this result to determine if the user should be allowed access or not.

Spring Security and reCAPTCHA
With Spring Security and reCAPTCHA the same logic still applies. To authenticate a user, the user must enter his username, password, and the two words that matches the CAPTCHA text images.

Here's a screenshot of our new login page:

After submitting the values, here's what should happen:
1. A custom filter stores the entered values from the CAPTCHA form for later use. (Scenario 2)
2. The web application queries the database to check if the entered username and password exist. If invalid, deny access. If valid, continue with the remaining filters. (Scenario 1)
3. A second custom filter retrieves the stored values from the CAPTCHA form and sends a POST request to the CAPTCHA service. (Scenario 2)

Development

The Filters

Our application has two custom filters:
1. CAPTCHA Capture filter 
2. CAPTCHA Verifier filter
The purpose of the Capture filter is to store the information entered by the user in the CAPTCHA form. Whereas the Verifier filter's purpose is to send a POST request to the CAPTCHA service and wait for the result. If the result is valid, allow the user to proceed; otherwise, it will show the login page again.

We'll place these two filters in-between the FORM_LOGIN_FILTER alias which is the one responsible for the form-based login for Spring Security. Why do we need to place these two filters in-between this alias?

<security:http auto-config="true" >
 ...
 <security:custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
 <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>
</security:http>

Issue 1:
Placing the capture and verifier filters after the FORM_LOGIN_FILTER resets the requests parameters which means they will always be null. The verifier has nothing to verify resulting to denied access!

Issue 2:
Placing the capture and verifier filter before the alias works but you won't be able to redirect the user to the login page if in case the user has entered an invalid information. You can throw an Exception like BadCredentialsException but the exception will be shown on the web page! Again, you can't redirect to a JSP page because the response stream has been already written.

Here's the capture filter:

CaptchaCaptureFilter.java
package org.krams.tutorial.filter;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Filter for capturing Captcha fields.
 * It's purpose is to store these values internally
 */
public class CaptchaCaptureFilter extends OncePerRequestFilter {
 
 protected Logger logger = Logger.getLogger("filter");
 private String recaptcha_response;
 private String recaptcha_challenge;
 private String remoteAddr;

 @Override
 public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
   FilterChain chain) throws IOException, ServletException {

  logger.debug("Captcha capture filter");
  
  // Assign values only when user has submitted a Captcha value.
  // Without this condition the values will be reset due to redirection
  // and CaptchaVerifierFilter will enter an infinite loop
  if (req.getParameter("recaptcha_response_field") != null) {
   recaptcha_response = req.getParameter("recaptcha_response_field");
   recaptcha_challenge = req.getParameter("recaptcha_challenge_field");
   remoteAddr = req.getRemoteAddr();
  }
  
  logger.debug("challenge: " + recaptcha_challenge);
  logger.debug("response: " + recaptcha_response);
  logger.debug("remoteAddr: " + remoteAddr);
  
  // Proceed with the remaining filters
  chain.doFilter(req, res);
 }

 public String getRecaptcha_response() {
  return recaptcha_response;
 }

 public void setRecaptcha_response(String recaptchaResponse) {
  recaptcha_response = recaptchaResponse;
 }

 public String getRecaptcha_challenge() {
  return recaptcha_challenge;
 }

 public void setRecaptcha_challenge(String recaptchaChallenge) {
  recaptcha_challenge = recaptchaChallenge;
 }

 public String getRemoteAddr() {
  return remoteAddr;
 }

 public void setRemoteAddr(String remoteAddr) {
  this.remoteAddr = remoteAddr;
 }
}

Here's the verifier filter:

CaptchaVerifierFilter.java
package org.krams.tutorial.filter;

import java.io.IOException;
import java.util.Properties;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.tanesha.recaptcha.ReCaptchaImpl;
import net.tanesha.recaptcha.ReCaptchaResponse;
import org.apache.log4j.Logger;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * Filter for verifying if the submitted Captcha fields
 * are valid. 
 * <p>
* This filter also allows you to set a proxy if needed
 */
public class CaptchaVerifierFilter extends OncePerRequestFilter {
 
 protected Logger logger = Logger.getLogger("filter");
 private Boolean useProxy = false;
 private String proxyPort;
 private String proxyHost;
 private String failureUrl;
 private CaptchaCaptureFilter captchaCaptureFilter;
 private String privateKey;
 
 // Inspired by log output: AbstractAuthenticationProcessingFilter.java:unsuccessfulAuthentication:320) 
 // Delegating to authentication failure handlerorg.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@15d4273
 private SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

 @Override
 public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
   FilterChain chain) throws IOException, ServletException {

  logger.debug("Captcha verifier filter");
  logger.debug("challenge: " + captchaCaptureFilter.getRecaptcha_challenge());
  logger.debug("response: " + captchaCaptureFilter.getRecaptcha_response());
  logger.debug("remoteAddr: " + captchaCaptureFilter.getRemoteAddr());
  
  // Assign values only when user has submitted a Captcha value
  if (captchaCaptureFilter.getRecaptcha_response() != null) {
   
   // Create a new recaptcha (by Soren Davidsen)
   ReCaptchaImpl reCaptcha = new ReCaptchaImpl();
   
   // Set the private key (assigned by Google)
   reCaptcha.setPrivateKey(privateKey);
 
   // Assign proxy if needed
   if (useProxy) {
    Properties systemSettings = System.getProperties();
    systemSettings.put("http.proxyPort",proxyPort);     
    systemSettings.put("http.proxyHost",proxyHost);
   }
   
   // Send HTTP request to validate user's Captcha
   ReCaptchaResponse reCaptchaResponse = reCaptcha.checkAnswer(captchaCaptureFilter.getRemoteAddr(), captchaCaptureFilter.getRecaptcha_challenge(), captchaCaptureFilter.getRecaptcha_response());
 
   // Check if valid
   if (!reCaptchaResponse.isValid()) {
    logger.debug("Captcha is invalid!");
    
          // Redirect user to login page
    failureHandler.setDefaultFailureUrl(failureUrl);
    failureHandler.onAuthenticationFailure(req, res, new BadCredentialsException("Captcha invalid!"));

   } else {
    logger.debug("Captcha is valid!");
   }
   
   // Reset Captcha fields after processing
   // If this method is skipped, everytime we access a page
   // CaptchaVerifierFilter will infinitely send a request to the Google Captcha service!
   resetCaptchaFields();
  }
  
  // Proceed with the remaining filters
  chain.doFilter(req, res);
 }

 /** 
  * Reset Captcha fields
  */
 public void resetCaptchaFields() {
  captchaCaptureFilter.setRemoteAddr(null);
  captchaCaptureFilter.setRecaptcha_challenge(null);
  captchaCaptureFilter.setRecaptcha_response(null);
 }
 
 public Boolean getUseProxy() {
  return useProxy;
 }

 public void setUseProxy(Boolean useProxy) {
  this.useProxy = useProxy;
 }

 public String getProxyPort() {
  return proxyPort;
 }

 public void setProxyPort(String proxyPort) {
  this.proxyPort = proxyPort;
 }

 public String getProxyHost() {
  return proxyHost;
 }

 public void setProxyHost(String proxyHost) {
  this.proxyHost = proxyHost;
 }

 public String getFailureUrl() {
  return failureUrl;
 }

 public void setFailureUrl(String failureUrl) {
  this.failureUrl = failureUrl;
 }

 public CaptchaCaptureFilter getCaptchaCaptureFilter() {
  return captchaCaptureFilter;
 }

 public void setCaptchaCaptureFilter(CaptchaCaptureFilter captchaCaptureFilter) {
  this.captchaCaptureFilter = captchaCaptureFilter;
 }

 public String getPrivateKey() {
  return privateKey;
 }

 public void setPrivateKey(String privateKey) {
  this.privateKey = privateKey;
 }
}

Configuration

We've already seen earlier the required configuration to activate the CAPTCHA filters. Here's the full spring-security.xml configuration file:

spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:security="http://www.springframework.org/schema/security"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/security 
   http://www.springframework.org/schema/security/spring-security-3.0.xsd">
 
 <security:http auto-config="true" use-expressions="true" access-denied-page="/krams/auth/denied" >
 
  <security:intercept-url pattern="/krams/auth/login" access="permitAll"/>
  <security:intercept-url pattern="/krams/main/admin" access="hasRole('ROLE_ADMIN')"/>
  <security:intercept-url pattern="/krams/main/common" access="hasRole('ROLE_USER')"/>
  
  <security:form-login
    login-page="/krams/auth/login" 
    authentication-failure-url="/krams/auth/login?error=true" 
    default-target-url="/krams/main/common"/>
   
  <security:logout 
    invalidate-session="true" 
    logout-success-url="/krams/auth/login" 
    logout-url="/krams/auth/logout"/>
 
  <security:custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
  <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>
 </security:http>
 
 <!-- For capturing CAPTCHA fields -->
 <bean id="captchaCaptureFilter" class="org.krams.tutorial.filter.CaptchaCaptureFilter" />
 
 <!-- For verifying CAPTCHA fields -->
 <!-- Private key is assigned by the reCATPCHA service -->
 <bean id="captchaVerifierFilter" class="org.krams.tutorial.filter.CaptchaVerifierFilter" 
    p:useProxy="false" 
    p:proxyPort="" 
    p:proxyHost=""
    p:failureUrl="/krams/auth/login?error=true"
    p:captchaCaptureFilter-ref="captchaCaptureFilter"
    p:privateKey="ADD-YOUR-PRIVATE-KEY-HERE"/>
 
 <!-- Declare an authentication-manager to use a custom userDetailsService -->
 <security:authentication-manager>
         <security:authentication-provider user-service-ref="userDetailsService">
           <security:password-encoder ref="passwordEncoder"/>
         </security:authentication-provider>
 </security:authentication-manager>
 
 <!-- Use a Md5 encoder since the user's passwords are stored as Md5 in the database -->
 <bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>

  <!-- An in-memory list of users. No need to access an external database layer.
      See Spring Security 3.1 Reference 5.2.1 In-Memory Authentication -->
  <!-- john's password is admin, while jane;s password is user  -->
  <security:user-service id="userDetailsService">
     <security:user name="john" password="21232f297a57a5a743894a0e4a801fc3" authorities="ROLE_USER, ROLE_ADMIN" />
     <security:user name="jane" password="ee11cbb19052e40b07aac0ca060c23ee" authorities="ROLE_USER" />
   </security:user-service>
 
</beans>
Notice the only changes we made here is add the two filters:
<security:http auto-config="true" >
 ...
 <security:custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
 <security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>
</security:http>

And declare the beans:
<!-- For capturing CAPTCHA fields -->
 <bean id="captchaCaptureFilter" class="org.krams.tutorial.filter.CaptchaCaptureFilter" />
 
 <!-- For verifying CAPTCHA fields -->
 <!-- Private key is assigned by the reCATPCHA service -->
 <bean id="captchaVerifierFilter" class="org.krams.tutorial.filter.CaptchaVerifierFilter" 
    p:useProxy="false" 
    p:proxyPort="" 
    p:proxyHost=""
    p:failureUrl="/krams/auth/login?error=true"
    p:captchaCaptureFilter-ref="captchaCaptureFilter"
    p:privateKey="ADD-YOUR-PRIVATE-KEY-HERE"/>
The verifier filter is configurable. You can enable proxy if needed and assign a failure URL which is usually the same URL declared in your form-login tag. Don't forget to add your private key which is provided freely by reCAPTCHA!

The Login Page

Our last task is modify the login page so that the CAPTCHA form is shown along with the username and password fields. To implement the login page we just mix directly the contents from the original loginpage.jsp and the one from http://code.google.com/apis/recaptcha/docs/java.html

Here's the login page:

loginpage.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ page import="net.tanesha.recaptcha.ReCaptcha" %>
<%@ page import="net.tanesha.recaptcha.ReCaptchaFactory" %>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>Login</h1>

<div id="login-error">${error}</div>

<c:url value="/j_spring_security_check" var="secureUrl"/>

<form action="${secureUrl}" method="post" >
    <%
        ReCaptcha c = ReCaptchaFactory.newReCaptcha("ADD-YOUR-PUBLIC-KEY-HERE", "ADD-YOUR-PRIVATE-KEY-HERE", false);
        out.print(c.createRecaptchaHtml(null, null));
    %>
 <p>
  <label for="j_username">Username</label>
  <input id="j_username" name="j_username" type="text" />
 </p>

 <p>
  <label for="j_password">Password</label>
  <input id="j_password" name="j_password" type="password" />
 </p>

 <input  type="submit" value="Login"/>   
     
</form>
        
</body>
</html>
The main changes here are the addition of the ReCaptcha:
<%
   ReCaptcha c = ReCaptchaFactory.newReCaptcha("ADD-YOUR-PUBLIC-KEY-HERE", "ADD-YOUR-PRIVATE-KEY-HERE", false);
   out.print(c.createRecaptchaHtml(null, null));
%>
Upon running the application, this will automatically build and show the CAPTCHA form.

Run the Application

To run the application, use the following URL:
http://localhost:8080/spring-security-recaptcha/
or to go directly to the login page:
http://localhost:8080/spring-security-recaptcha/krams/auth/login

Reminders:
1. Make sure you've signed-up for a reCAPTCHA account!
2. Don't forget to add your Public and Private keys in the JSP page
3. Don't forget to add the Private key in the spring-security.xml
4. Enable the proxy setting if you have one

The application has two built-in users:
username: john / password: admin
username: jane / password: user

Conclusion

That's it. We've successfully integrated reCAPTCHA with an existing Spring Security application without changing any of the classes. We've modified our configuration by just adding two simple custom filters. We've also successfully followed the guidelines presented in Using reCAPTCHA with Java/JSP article.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-security-recaptcha/

You can download the project as a Maven build. Look for the spring-security-recaptcha.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Security 3: Integrating reCAPTCHA Service ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

Saturday, February 19, 2011

Spring Data - MongoDB Tutorial (1.0.0.M1)

Introduction

In this tutorial we will refactor an existing Spring MVC 3 - MongoDB application (see here) to use the newly released Spring Data Document 1.0.0.M1 for MongoDB. Our purpose here is to realize how Spring Data simplifies integration development with MongoDB. To appreciate Spring Data, it is advisable to know how to perform basic CRUD functions with native MongoDB support. And that's the reason why we'll be refactoring an existing Spring MVC 3 - MongoDB application.

Note: An updated version of this tutorial is now accessible at Spring MVC 3.1 - Implement CRUD with Spring Data MongoDB (Part 1)

What is MongoDB?
MongoDB (from "humongous") is a scalable, high-performance, open source, document-oriented database. Written in C++, MongoDB features:
  • Document-oriented storage
  • Full Index Support
  • Replication & High Availability
  • Scale horizontally without compromising functionality.
  • Rich, document-based queries.
  • Atomic modifiers for contention-free performance.
  • Flexible aggregation and data processing.
  • Store files of any size without complicating your stack.
  • Enterprise class support, training, and consulting available.

Source: http://www.mongodb.org/

What is Spring Data - Document?
The Spring Data Document (or DATADOC) framework makes it easy to write Spring applications that use a Document store by eliminating the redundant tasks and boiler place code required for interacting with the store through Spring's excellent infrastructure support.

Source: Spring Datastore Document - Reference Documentation
In a nutshell MongoDB uses JSON instead of SQL There's no static schema to create. All schemas are dynamic, meaning you create them on-the-fly. You can try a real-time online shell for MongoDB at http://try.mongodb.org/. Visit the official MongoDB site for a thorough discussion.

Prerequisites
In order to complete this tutorial, you will be required to install a copy of MongoDB. If you don't have one yet, grab a copy now by visiting http://www.mongodb.org/display/DOCS/Quickstart. Installation is really easy.

Development

Our application is a simple CRUD system for managing a list of Persons. Data is stored in MongoDB database. We'll start by declaring our domain objects. Then we'll discuss the service layer. And lastly we'll add the controllers.

The Domain Layer

Our application contains a single domain object named Person. It consists the following properties:
pid
firstName
lastName
money
Here's the class declaration:

Person.java
package org.krams.tutorial.domain;

import java.io.Serializable;

/**
 * A simple POJO representing a Person
 * 
 * @author Krams at {@link http://krams915@blogspot.com}
 */
public class Person implements Serializable {

 private static final long serialVersionUID = -5527566248002296042L;
 
 private String pid;
 private String firstName;
 private String lastName;
 private Double money;

 public String getPid() {
  return pid;
 }

 public void setPid(String pid) {
  this.pid = pid;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public Double getMoney() {
  return money;
 }

 public void setMoney(Double money) {
  this.money = money;
 }
}
Warning!
In the original tutorial we used id to signify the primary identity of the Person. But in this tutorial we used pid instead. This is because when we use id with Spring Data, it messes up the id property by merging it with the built-in _id property of MongoDB.

For now, if you plan to use Spring Data, don't use id as your id field. Choose a different name instead. I have filed already an inquiry about this behavior in the Spring forums. See it here.

The Service Layer

Our service class contains the main changes in the original application. Instead of calling native MongoDB methods for performing CRUD operations, we use Spring Data's MongoTemplate instead.

What is MongoTemplate?
The template offers convenience methods and automatic mapping between MongoDB JSON documents and your domain classes. Out of the box, MongoTemplate uses a Java-based default converter but you can also write your own converter classes to be used for reading and storing domain objects.

Source: Spring Datastore Document - Reference Documentation

Here's the class declaration:

PersonService.java
package org.krams.tutorial.service;

import java.util.List;
import java.util.UUID;

import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.springframework.data.document.mongodb.MongoTemplate;
import org.springframework.data.document.mongodb.query.Query;
import org.springframework.data.document.mongodb.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.data.document.mongodb.query.Criteria.where;

/**
 * Service for processing {@link Person} objects.
 * Uses Spring's {@link MongoTemplate} to perform CRUD operations.
 * <p>
 * For a complete reference to MongoDB
 * see http://www.mongodb.org/
 * <p>
 * For a complete reference to Spring Data MongoDB 
 * see http://www.springsource.org/spring-data
 * 
 * @author Krams at {@link http://krams915@blogspot.com}
 */
@Service("personService")
@Transactional
public class PersonService {

 protected static Logger logger = Logger.getLogger("service");
 
 @Resource(name="mongoTemplate")
 private MongoTemplate mongoTemplate;
 
 /**
  * Retrieves all persons
  */
 public List<Person> getAll() {
  logger.debug("Retrieving all persons");
 
  // Find an entry where pid property exists
        Query query = new Query(where("pid").exists(true));
        // Execute the query and find all matching entries
        List<Person> persons = mongoTemplate.find(query, Person.class);
        
  return persons;
 }
 
 /**
  * Retrieves a single person
  */
 public Person get( String id ) {
  logger.debug("Retrieving an existing person");
  
  // Find an entry where pid matches the id
        Query query = new Query(where("pid").is(id));
        // Execute the query and find one matching entry
        Person person = mongoTemplate.findOne("mycollection", query, Person.class);
     
  return person;
 }
 
 /**
  * Adds a new person
  */
 public Boolean add(Person person) {
  logger.debug("Adding a new user");
  
  try {
   
   // Set a new value to the pid property first since it's blank
   person.setPid(UUID.randomUUID().toString());
   // Insert to db
      mongoTemplate.insert("mycollection", person);

   return true;
   
  } catch (Exception e) {
   logger.error("An error has occurred while trying to add new user", e);
   return false;
  }
 }
 
 /**
  * Deletes an existing person
  */
 public Boolean delete(String id) {
  logger.debug("Deleting existing person");
  
  try {
   
   // Find an entry where pid matches the id
         Query query = new Query(where("pid").is(id));
         // Run the query and delete the entry
         mongoTemplate.remove(query);
         
   return true;
   
  } catch (Exception e) {
   logger.error("An error has occurred while trying to delete new user", e);
   return false;
  }
 }
 
 /**
  * Edits an existing person
  */
 public Boolean edit(Person person) {
  logger.debug("Editing existing person");
  
  try {
   
   // Find an entry where pid matches the id
         Query query = new Query(where("pid").is(person.getPid()));
         
   // Declare an Update object. 
         // This matches the update modifiers available in MongoDB
   Update update = new Update();
         
         update.set("firstName", person.getFirstName());
         mongoTemplate.updateMulti(query, update);
         
         update.set("lastName", person.getLastName());
         mongoTemplate.updateMulti(query, update);
         
         update.set("money", person.getMoney());
         mongoTemplate.updateMulti(query, update);
         
   return true;
   
  } catch (Exception e) {
   logger.error("An error has occurred while trying to edit existing user", e);
   return false;
  }
  
 }
}
The code should should be self-explanatory. Notice how Spring Data has reduced the amount of code. To appreciate this difference, let's do a comparison between using the traditional MongoDB and using Spring Data.

Retrieving all entries

old implementation
public List<person> getAll() {
  logger.debug("Retrieving all persons");
   
  // Retrieve collection
  DBCollection coll = MongoDBFactory.getCollection("mydb","mycollection");
  // Retrieve cursor for iterating records
     DBCursor cur = coll.find();
     // Create new list
  List<person> items = new ArrayList<person>();
  // Iterate cursor
        while(cur.hasNext()) {
         // Map DBOject to Person
         DBObject dbObject = cur.next();
         Person person = new Person();
          
         person.setId(dbObject.get("id").toString());
         person.setFirstName(dbObject.get("firstName").toString());
         person.setLastName(dbObject.get("lastName").toString());
         person.setMoney(Double.valueOf(dbObject.get("money").toString()));
 
         // Add to new list
         items.add(person);
        }
         
        // Return list
  return items;
 }

new implementation
public List<Person> getAll() {
  logger.debug("Retrieving all persons");
 
        Query query = new Query(where("pid").exists(true));
        List<Person> persons = mongoTemplate.find(query, Person.class);
        
  return persons;
 }

Retrieving a single entry

old implementation
public Person get( String id ) {
  logger.debug("Retrieving an existing person");
   
  // Retrieve collection
  DBCollection coll = MongoDBFactory.getCollection("mydb","mycollection");
  // Create a new object
  DBObject doc = new BasicDBObject();
  // Put id to search
        doc.put("id", id);
         
        // Find and return the person with the given id
        DBObject dbObject = coll.findOne(doc);
         
        // Map DBOject to Person
     Person person = new Person();
     person.setId(dbObject.get("id").toString());
     person.setFirstName(dbObject.get("firstName").toString());
     person.setLastName(dbObject.get("lastName").toString());
     person.setMoney(Double.valueOf(dbObject.get("money").toString()));
      
        // Return person
  return person;
 }

new implementation
public Person get( String id ) {
  logger.debug("Retrieving an existing person");
  
  // Find an entry where pid matches the id
        Query query = new Query(where("pid").is(id));
        // Execute the query and find one matching entry
        Person person = mongoTemplate.findOne("mycollection", query, Person.class);
     
  return person;
 }

Adding a new entry

old implementation
public Boolean add(Person person) {
  logger.debug("Adding a new user");
   
  try {
   // Retrieve collection
   DBCollection coll = MongoDBFactory.getCollection("mydb","mycollection");
   // Create a new object
   BasicDBObject doc = new BasicDBObject();
   // Generate random id using UUID type 4
   // See http://en.wikipedia.org/wiki/Universally_unique_identifier
         doc.put("id", UUID.randomUUID().toString() ); 
         doc.put("firstName", person.getFirstName());
         doc.put("lastName", person.getLastName());
         doc.put("money", person.getMoney());
         // Save new person
         coll.insert(doc);
          
   return true;
    
  } catch (Exception e) {
   logger.error("An error has occurred while trying to add new user", e);
   return false;
  }
 }

new implementation
public Boolean add(Person person) {
  logger.debug("Adding a new user");
  
  try {
   
   // Set a new value to the pid property first since it's blank
   person.setPid(UUID.randomUUID().toString());
   // Insert to db
      mongoTemplate.insert("mycollection", person);

   return true;
   
  } catch (Exception e) {
   logger.error("An error has occurred while trying to add new user", e);
   return false;
  }
 }

Deleting an entry

old implementation
public Boolean delete(String id) {
  logger.debug("Deleting existing person");
   
  try {
   // Retrieve person to delete
   BasicDBObject item = (BasicDBObject) getDBObject( id );
   // Retrieve collection
   DBCollection coll = MongoDBFactory.getCollection("mydb","mycollection");
   // Delete retrieved person
         coll.remove(item);
          
   return true;
    
  } catch (Exception e) {
   logger.error("An error has occurred while trying to delete new user", e);
   return false;
  }
 }

new implementation
public Boolean delete(String id) {
  logger.debug("Deleting existing person");
  
  try {
   
   // Find an entry where pid matches the id
         Query query = new Query(where("pid").is(id));
         // Run the query and delete the entry
         mongoTemplate.remove(query);
         
   return true;
   
  } catch (Exception e) {
   logger.error("An error has occurred while trying to delete new user", e);
   return false;
  }
 }

Updating an entry

old implementation
public Boolean edit(Person person) {
  logger.debug("Editing existing person");
   
  try {
   // Retrieve person to edit
   BasicDBObject existing = (BasicDBObject) getDBObject( person.getId() );
    
   DBCollection coll = MongoDBFactory.getCollection("mydb","mycollection");
    
   // Create new object
   BasicDBObject edited = new BasicDBObject();
   // Assign existing details
   edited.put("id", person.getId()); 
   edited.put("firstName", person.getFirstName());
   edited.put("lastName", person.getLastName());
   edited.put("money", person.getMoney());
   // Update existing person
         coll.update(existing, edited);
          
   return true;
    
  } catch (Exception e) {
   logger.error("An error has occurred while trying to edit existing user", e);
   return false;
  }
   
 }

new implementation
public Boolean edit(Person person) {
  logger.debug("Editing existing person");
  
  try {
   
   // Find an entry where pid matches the id
         Query query = new Query(where("pid").is(person.getPid()));
         
   // Declare an Update object. 
         // This matches the update modifiers available in MongoDB
   Update update = new Update();
         
         update.set("firstName", person.getFirstName());
         mongoTemplate.updateMulti(query, update);
         
         update.set("lastName", person.getLastName());
         mongoTemplate.updateMulti(query, update);
         
         update.set("money", person.getMoney());
         mongoTemplate.updateMulti(query, update);
         
   return true;
   
  } catch (Exception e) {
   logger.error("An error has occurred while trying to edit existing user", e);
   return false;
  }
  
 }

Configuration

To use Spring's MongoTemplate it needs to be declared via configuration. It also needs a reference to a MongoDB database. Let's declare an XML configuration that satifies these requirements:

mongo-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:p="http://www.springframework.org/schema/p" 
    xmlns:mongo="http://www.springframework.org/schema/data/mongo"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/data/mongo
      http://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd">
 
 <!-- Default bean name is 'mongo' -->
 <mongo:mongo host="localhost" port="27017"/>
 
 <!-- Offers convenience methods and automatic mapping between MongoDB JSON documents and your domain classes. -->
   <bean id="mongoTemplate" class="org.springframework.data.document.mongodb.MongoTemplate">
     <constructor-arg ref="mongo"/>
     <constructor-arg value="mydb"/>
     <constructor-arg value="mycollection"/>
   </bean>
   
   <bean id="initService" class="org.krams.tutorial.service.InitService" init-method="init"></bean>
</beans>
Notice we're using the mongo namespace:
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
We've declared a reference to a MongoDB database by declaring:
<mongo:mongo host="localhost" port="27017"/>

Then we declared a MongoTemplate that references a MongoDB database (mongo), a database (mydb), and a collection (mycollection):
<bean id="mongoTemplate" class="org.springframework.data.document.mongodb.MongoTemplate">
     <constructor-arg ref="mongo"/>
     <constructor-arg value="mydb"/>
     <constructor-arg value="mycollection"/>
   </bean>

Lastly, we declared an initService
<bean id="initService" class="org.krams.tutorial.service.InitService" init-method="init"></bean>
The purpose of the initService is to prepopulate our MongoDB with sample data.

Here's the class declaration:

InitService.java
package org.krams.tutorial.service;

import java.util.UUID;

import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.springframework.data.document.mongodb.MongoTemplate;
import org.springframework.transaction.annotation.Transactional;

/**
 * Service for initializing MongoDB with sample data
 * <p>
 * For a complete reference to MongoDB
 * see http://www.mongodb.org/
 * <p>
 * For transactions, see http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/transaction.html
 * 
 * @author Krams at {@link http://krams915@blogspot.com}
 */
@Transactional
public class InitService {

 protected static Logger logger = Logger.getLogger("service");
 
 @Resource(name="mongoTemplate")
 private MongoTemplate mongoTemplate;

 private void init() {
  // Populate our MongoDB database
  logger.debug("Init MongoDB users");
  
  // Drop existing collection
  mongoTemplate.dropCollection("mycollection");
  
  // Create new object
  Person p = new Person ();
  p.setPid(UUID.randomUUID().toString());
  p.setFirstName("John");
  p.setLastName("Smith");
  p.setMoney(1000.0);
  
  // Insert to db
     mongoTemplate.insert("mycollection", p);

     // Create new object
  p = new Person ();
  p.setPid(UUID.randomUUID().toString());
  p.setFirstName("Jane");
  p.setLastName("Adams");
  p.setMoney(2000.0);
  
  // Insert to db
     mongoTemplate.insert("mycollection", p);
        
     // Create new object
  p = new Person ();
  p.setPid(UUID.randomUUID().toString());
  p.setFirstName("Jeff");
  p.setLastName("Mayer");
  p.setMoney(3000.0);
  
  // Insert to db
     mongoTemplate.insert("mycollection", p);
 }
}

The Controller Layer

After creating the domain and service classes, we need to declare a controller that will handle the web requests.

MainController.java
package org.krams.tutorial.controller;

import java.util.List;
import javax.annotation.Resource;
import org.apache.log4j.Logger;
import org.krams.tutorial.domain.Person;
import org.krams.tutorial.service.PersonService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;


/**
 * Handles and retrieves person request
 * 
 * @author Krams at {@link http://krams915@blogspot.com}
 */
@Controller
@RequestMapping("/main")
public class MainController {

 protected static Logger logger = Logger.getLogger("controller");
 
 @Resource(name="personService")
 private PersonService personService;
 
 /**
  * Handles and retrieves all persons and show it in a JSP page
  * 
  * @return the name of the JSP page
  */
    @RequestMapping(value = "/persons", method = RequestMethod.GET)
    public String getPersons(Model model) {
     
     logger.debug("Received request to show all persons");
     
     // Retrieve all persons by delegating the call to PersonService
     List persons = personService.getAll();
     
     // Attach persons to the Model
     model.addAttribute("persons", persons);
     
     // This will resolve to /WEB-INF/jsp/personspage.jsp
     return "personspage";
 }
    
    /**
     * Retrieves the add page
     * 
     * @return the name of the JSP page
     */
    @RequestMapping(value = "/persons/add", method = RequestMethod.GET)
    public String getAdd(Model model) {
     logger.debug("Received request to show add page");
    
     // Create new Person and add to model
     // This is the formBackingOBject
     model.addAttribute("personAttribute", new Person());

     // This will resolve to /WEB-INF/jsp/addpage.jsp
     return "addpage";
 }
 
    /**
     * Adds a new person by delegating the processing to PersonService.
     * Displays a confirmation JSP page
     * 
     * @return  the name of the JSP page
     */
    @RequestMapping(value = "/persons/add", method = RequestMethod.POST)
    public String add(@ModelAttribute("personAttribute") Person person) {
  logger.debug("Received request to add new person");
  
     // The "personAttribute" model has been passed to the controller from the JSP
     // We use the name "personAttribute" because the JSP uses that name
  
  // Call PersonService to do the actual adding
  personService.add(person);

     // This will resolve to /WEB-INF/jsp/addedpage.jsp
  return "addedpage";
 }
    
    /**
     * Deletes an existing person by delegating the processing to PersonService.
     * Displays a confirmation JSP page
     * 
     * @return  the name of the JSP page
     */
    @RequestMapping(value = "/persons/delete", method = RequestMethod.GET)
    public String delete(@RequestParam(value="pid", required=true) String id, 
              Model model) {
   
  logger.debug("Received request to delete existing person");
  
  // Call PersonService to do the actual deleting
  personService.delete(id);
  
  // Add id reference to Model
  model.addAttribute("pid", id);
     
     // This will resolve to /WEB-INF/jsp/deletedpage.jsp
  return "deletedpage";
 }
    
    /**
     * Retrieves the edit page
     * 
     * @return the name of the JSP page
     */
    @RequestMapping(value = "/persons/edit", method = RequestMethod.GET)
    public String getEdit(@RequestParam(value="pid", required=true) String id,  
              Model model) {
     logger.debug("Received request to show edit page");
    
     // Retrieve existing Person and add to model
     // This is the formBackingOBject
     model.addAttribute("personAttribute", personService.get(id));
     
     // This will resolve to /WEB-INF/jsp/editpage.jsp
     return "editpage";
 }
    
    /**
     * Edits an existing person by delegating the processing to PersonService.
     * Displays a confirmation JSP page
     * 
     * @return  the name of the JSP page
     */
    @RequestMapping(value = "/persons/edit", method = RequestMethod.POST)
    public String saveEdit(@ModelAttribute("personAttribute") Person person, 
                 @RequestParam(value="pid", required=true) String id, 
                Model model) {
     logger.debug("Received request to update person");
    
     // The "personAttribute" model has been passed to the controller from the JSP
     // We use the name "personAttribute" because the JSP uses that name
     
     // We manually assign the id because we disabled it in the JSP page
     // When a field is disabled it will not be included in the ModelAttribute
     person.setPid(id);
     
     // Delegate to PersonService for editing
     personService.edit(person);
     
     // Add id reference to Model
  model.addAttribute("pid", id);
  
     // This will resolve to /WEB-INF/jsp/editedpage.jsp
  return "editedpage";
 }
    
}
Our controller is a simple class that delegates actual processing to PersonService. When the service is done processing, the controller forwards the result to a JSP view.

Other Configurations and Files

To make the tutorial manageable, I've decided not to post the following configuration files in this tutorial:
web.xml
spring-servlet.xml
applicationContext.xml
These files are standard Spring MVC related configuration files. You can find them in the downloadable application at the end of this tutorial.

I have also left out the JSP declarations. You can find a description of them in the following tutorial: Spring MVC 3: Using a Document-Oriented Database - MongoDB

Run the Application

To run the application, open your browser and enter the following URL:
http://localhost:8080/spring-data-mongodb/krams/main/persons
You should see the following CRUD view:


Conclusion

That's it. We have successfully refactored our existing Spring MVC 3 - MongoDB application to use the newly released Spring Data Document 1.0 for MongoDB. We've compared side-by-side between the native MongoDB development and with the new Spring Data framework. We have seen how Spring Data has simplified our development further.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-mvc-mongodb/

You can download the project as a Maven build. Look for the spring-data-mongodb.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring Data - MongoDB Tutorial (1.0.0.M1) ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share