Showing posts with label REST. Show all posts
Showing posts with label REST. Show all posts

Tuesday, May 28, 2013

Restful Generic DAO with Spring 3 + JPA + unit testing + integration testing

This time, we will create a generic DAO layer, a Service layer and a REST layer which exposes the Service layer to whoever wants to consume it. Unit tests and Integration tests will be created too. Everything is implemented with Spring 3.

Versions used:
  1. Spring 3.2.2.RELEASE
  2. JAXB
  3. Hibernate 4.2.1.Final
  4. Maven 3

pom.xml
<dependencies>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>3.2.2.RELEASE</version>
  </dependency>
  
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>3.2.2.RELEASE</version>
  </dependency>

  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-entitymanager</artifactId>
   <version>4.2.1.Final</version>
  </dependency>

  <dependency>
   <groupId>hsqldb</groupId>
   <artifactId>hsqldb</artifactId>
   <version>1.8.0.10</version>
  </dependency>

  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.17</version>
  </dependency>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.11</version>
   <scope>test</scope>
  </dependency>
  
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>3.2.2.RELEASE</version>
   <scope>test</scope>
  </dependency>

  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
   <scope>test</scope>
  </dependency>

 </dependencies>

hsqldb will be our in-memory database. Let´s see the rest of the relevant pom info.

<!-- Jetty plugin -->
 <plugin>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>maven-jetty-plugin</artifactId>
  <version>6.1.10</version>
  <configuration>
   <scanIntervalSeconds>10</scanIntervalSeconds>
  </configuration>
 </plugin>
With the Maven Jetty Plugin you can manually run mvn jetty:run in your terminal and the server will be started. Now that the server is up, it lets you run the REST services tests (those tests are considered integration tests). Below you can find the automated way to treat integration tests just like normal unit tests are treated.

<!-- This one is to run integration tests -->
 <!-- By default runs all *IT.java tests -->
 <plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>2.12</version>
  <executions>
   <execution>
    <goals>
     <goal>integration-test</goal>
     <goal>verify</goal>
    </goals>
   </execution>
  </executions>
 </plugin>
Similar to running mvn test to run the unit tests, when you run mvn verify, integration tests are executed. But what if you want to run unit tests and integration tests when you compile your project? Should you run mvn test verify in that case? Well, you must tell maven to start the jetty server right after unit tests finish but before integration tests run (no, you cannot use mvn test jetty:run verify)... see below:

<!-- Responsible for starting jetty before integration tests -->
 <plugin>
  <groupId>org.codehaus.cargo</groupId>
  <artifactId>cargo-maven2-plugin</artifactId>
  <version>1.2.0</version>
  <configuration>
   <container>
    <containerId>jetty6x</containerId>
    <type>embedded</type>
   </container>
  </configuration>
   <executions>
    <execution>
     <id>start-jetty</id>
     <phase>pre-integration-test</phase>
     <goals>
     <goal>start</goal>
     </goals>
    </execution>
    <execution>
     <id>stop-jetty</id>
     <phase>post-integration-test</phase>
     <goals>
     <goal>stop</goal>
     </goals>
   </execution>
  </executions>
 </plugin>
Pretty straightforward, right? Thanks to the Cargo plugin, whenever integration tests are executed, in the pre-integration-test maven phase the jetty server will be automatically started.

web.xml
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath*:spring-beans.xml</param-value>
 </context-param>

 <context-param>
  <param-name>log4jConfigLocation</param-name>
  <param-value>classpath*:log4j.properties</param-value>
 </context-param>
 <servlet>
  <servlet-name>applicationContext</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
  <servlet-name>applicationContext</servlet-name>
  <url-pattern>/client-crud/*</url-pattern>
 </servlet-mapping>
 <listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
That's a normal Spring setup.

applicationContext-servlet.xml
<!-- Enabling Spring beans auto-discovery -->
<context:component-scan base-package="ar.com.pabloExample" />
<!-- Enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven transaction-manager="txManager" />
<!-- Enabling Spring MVC configuration through annotations -->
<mvc:annotation-driven />
In that XML we configure our beans to be scanned because we will use annotations. The transaction manager is enabled to work with annotations too.

GenericDao
public interface GenericDao<T extends Serializable> {
 
 public long count();
 public T create(T t);
 public void delete(Object id);
 public T find(Object id);
 public List<T> getAll();
 public T update(T t); 
}
That's a generic DAO interface.

ClientDao
public interface ClientDao extends GenericDao<Client> {

}
For every entity we want to use the generic DAO, we must create the interface for that entity because the generic DAO is GenericDao<?> and we must tell it which is the entity it will accept, in this case GenericDao<Client>

GenericDaoImpl
public abstract class GenericDaoImpl<T extends Serializable> implements GenericDao<T> {
 
 private Class<T> type;
 
 @SuppressWarnings("unchecked")
 public GenericDaoImpl() {
  Type t = getClass().getGenericSuperclass();
  ParameterizedType pt = (ParameterizedType) t;
  type = (Class<T>) pt.getActualTypeArguments()[0];
 }
 
 @PersistenceContext
 protected EntityManager em;

 @Override
 public long count() {
  String entity = type.getSimpleName();
  final StringBuffer queryString = new StringBuffer("select count(ent) from " + entity + " ent");
  final Query query = this.em.createQuery(queryString.toString());
  return (Long) query.getSingleResult();
 }

 @Override
 public T create(final T t) {
  em.persist(t);
  return t;
 }

 @Override
 public void delete(final Object id) {
  em.remove(em.getReference(type, id));
 }

 @Override
 public T find(final Object id) {
  return em.find(type, id);
 }

 @Override
 public T update(final T t) {
  return em.merge(t);
 }

 @SuppressWarnings("unchecked")
 @Override
 public List<T> getAll() {
  Query query = em.createQuery("from " + type.getName());
  return query.getResultList();
 }
}
This is the implementation of the generic DAO with Hibernate.

ClientDaoImpl
@Repository
public class ClientDaoImpl extends GenericDaoImpl<Client> implements ClientDao {

}
Similar to what we did with the ClientDao interface, we have to do it with the implementation too. We must create the ClientDao implementation and make it extend the generic Dao implementation.
The @Repository annotation only tells Spring this is a bean the "annotations way" of type "DAO" (see the end of the post to understand more of it).

ClientService
public interface ClientService {
 
 public Client create(Client client);
 public void delete(Integer id);
 public Client update(Client client);
 public Client find(Integer id);
 public List<Client> getAll();
 public Long count();
}
This is the  Service layer. It can be generic too, altough in this example only the DAO layer is generic.

ClientServiceImpl
@Service
public class ClientServiceImpl implements ClientService {
 
 @Autowired
 private ClientDao clientDao;

 @Override
 @Transactional(readOnly = false)
 public Client create(Client client) {
  return clientDao.create(client);
 }

 @Override
 @Transactional(readOnly = true)
 public List<Client> getAll() {
  return clientDao.getAll();
 }

 @Override
 @Transactional(readOnly = false)
 public void delete(Integer id) {
  clientDao.delete(id);
 }

 @Override
 @Transactional(readOnly = false)
 public Client update(Client client) {
  return clientDao.update(client);
 }

 @Override
 @Transactional(readOnly = false)
 public Client find(Integer id) {
  return clientDao.find(id);
 }

 @Override
 @Transactional(readOnly = true)
 public Long count() {
  return clientDao.count();
 }

}
In this class, all of our methods are transactional, that's why we annotate them with @Transactional. With @Autorwired we inject the Client DAO implementation. The @Service does the same as @Repository; It tells Spring that this class is a bean too (check the end of the post for more information about it).

spring-beans.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"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

 <!-- Datasource configured in spring and not in persitence.xml -->
 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
  <property name="url" value="jdbc:hsqldb:mem:testdb" />
  <property name="username" value="sa" />
  <property name="password" value="" />
 </bean>
 
 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
  <property name="persistenceXmlLocation" value="classpath:persistence.xml"/>
  <property name="persistenceUnitName" value="spring-webapp-persistenceUnit" />
  <property name="dataSource" ref="dataSource"/>
 </bean>

 <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
  <property name="entityManagerFactory" ref="entityManagerFactory"/>
  <property name="dataSource" ref="dataSource" />
 </bean>

</beans>
This file is a normal one, we define the datasource, the hibernate entity manager and the transaction manager associated with both.

ClientRestService
@Controller
@RequestMapping("/rest-services/clients")
public class ClientRestService {
 
 private static final Logger LOGGER = Logger.getLogger(ClientService.class);
 
 @Autowired
 private ClientService clientService;

 @RequestMapping(method = RequestMethod.GET)
 @ResponseBody
 public Clients loadClients() {
  
  Clients clientsWrapper = new Clients();
  clientsWrapper.setClients(clientService.getAll());
  
  LOGGER.info("--- Client list retrieved ---");
  return clientsWrapper;
 }
 
 @RequestMapping(method = RequestMethod.POST)
 @ResponseBody
 public Client addClient(@RequestBody Client client) {
  
  Client newClient = clientService.create(client);
  
  LOGGER.info("--- New client saved ---");
  return newClient;
 }
 
 @RequestMapping(value = "/{clientId}", method = RequestMethod.PUT)
 @ResponseBody
 public Client updateClient(@PathVariable(value = "clientId") Integer clientId, @RequestBody Client client) {
  
  Client newClient = clientService.update(client);
  
  LOGGER.info("--- Client updated ---");
  return newClient;
 }
 
 @RequestMapping(value = "/{clientId}", method = RequestMethod.GET)
 @ResponseBody
 public Client findClient(@PathVariable(value = "clientId") Integer clientId) {
  
  Client foundClient = clientService.find(clientId);
  
  LOGGER.info("--- Client found ---");
  return foundClient;
 }
 
 @RequestMapping(value = "/{clientId}", method = RequestMethod.DELETE)
 @ResponseBody
 public void deleteClient(@PathVariable(value = "clientId") Integer clientId) {
  
  clientService.delete(clientId);
  LOGGER.info("--- Client deleted ---");
 }
 
}
Here we expose our service layer (in this case, our class annotated with @Service) via REST. To sum up the operations available, you can see this table:

URL Method type Operation
http://localhost:8080/spring-webapp-example/client-crud/rest-services/clients GET get all clients
http://localhost:8080/spring-webapp-example/client-crud/rest-services/clients POST create client
http://localhost:8080/spring-webapp-example/client-crud/rest-services/clients/1 PUT update client id = 1
http://localhost:8080/spring-webapp-example/client-crud/rest-services/clients/1 GET find client id = 1
http://localhost:8080/spring-webapp-example/client-crud/rest-services/clients/1 DELETE delete client id = 1

@Controller does the same job as the previous @Repository and @Service annotations; It tells Spring that this class is a Spring bean.

Clients
@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
public class Clients {
 
 private List<Client> clients;

 @XmlElement(name = "client")
 public List<Client> getClients() {
  return clients;
 }

 public void setClients(List<Client> clients) {
  this.clients = clients;
 }

}
This class acts as a wrapper for the list of client class. That wrapper eases the process of consuming a REST service that returns a list of objects. The wrapper class is annotated with JAXB annotations

Client
@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
@Entity
@Table(name = "T_CLIENT")
public class Client implements Serializable {

 private static final long serialVersionUID = -7682472386786656877L;
 
 @Id
 @GeneratedValue(strategy = GenerationType.SEQUENCE)
 private Integer id;
 private String name;

 public Integer getId() {
  return id;
 }

 public void setId(Integer id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

}
The Client JPA entity which is used by Hibernate, is annotated with JAXB elements too so that we can reuse the same entity when exposing it via REST. Of course you may create another Client class in order to expose only the information you want to expose.

Let's test this example!


ClientServiceTest
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/spring-beans-test.xml", "/applicationContext-servlet-test.xml" })
public class ClientServiceTest {
 
 @Autowired
 private ClientService clientService;
 
 private Client createNewClient() {
  Client client = new Client();
  client.setName("Rupert");
  
  return client;
 }
 
 @Test
 public void createTest() {
  
  Client client = createNewClient();
  
  Client clientSaved = clientService.create(client);
  
  Assert.assertNotNull(clientSaved);
  Assert.assertNotNull(clientSaved.getId());
 }
 
 @Test
 public void deleteTest() {
  Client client = createNewClient();
  
  Client clientSaved = clientService.create(client);
  clientService.delete(clientSaved.getId());
  Client clientDeleted = clientService.find(clientSaved.getId());

  Assert.assertNull(clientDeleted);
 }
 
 @Test
 public void findTest() {
  Client client = createNewClient();
  
  Client clientSaved = clientService.create(client);
  Client clientDeleted = clientService.find(clientSaved.getId());

  Assert.assertNotNull(clientDeleted);
 }
 
 @Test
 public void countTest() {
  Client client = createNewClient();
  
  long countBefore = clientService.count();
  clientService.create(client);
  long countAfter = clientService.count();
  
  Assert.assertEquals(countBefore, countAfter - 1);
 }
 
 @Test
 public void getAllTest() {
  this.createTest();
  List<Client> all = clientService.getAll();
  
  Assert.assertNotNull(all);
  Assert.assertTrue(all.size() > 0);
 }

}
This unit test is for the Client Service layer. You can run it with mvn test command.

ClientRestServiceIT
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/spring-beans-test.xml", "/applicationContext-servlet-test.xml" })
public class ClientRestServiceIT {
 
 private static final String REST_SERVICE_URL = "http://localhost:8080/spring-webapp-example/client-crud/rest-services/clients";
 private RestTemplate restTemplate = new RestTemplate();
 
 private Client createNewClient() {
  Client client = new Client();
  client.setName("Rupert");
  
  return client;
 }
 
 @Test
 public void createTest() {
  
  Client newClient = createNewClient();
  Client clientCreated = restTemplate.postForObject(REST_SERVICE_URL, newClient, Client.class);
  
  Assert.assertNotNull(clientCreated);
  Assert.assertNotNull(clientCreated.getId());
 }
 
 @Test
 public void updateTest() {
  
  Client newClient = createNewClient();
  Client clientCreated = restTemplate.postForObject(REST_SERVICE_URL, newClient, Client.class);
  
  clientCreated.setName("Stuart");
  
  String restServiceUrl = REST_SERVICE_URL + "/" + clientCreated.getId();
  restTemplate.put(restServiceUrl, clientCreated);
  Client clientModified = restTemplate.getForObject(restServiceUrl, Client.class);
  
  Assert.assertNotNull(clientModified);
  Assert.assertTrue(clientModified.getName().equals("Stuart"));
 }
 
 @Test
 public void findTest() {
  
  Client newClient = createNewClient();
  Client clientCreated = restTemplate.postForObject(REST_SERVICE_URL, newClient, Client.class);
  
  String restServiceUrlFind = REST_SERVICE_URL + "/" + clientCreated.getId();
  Client clientFound = restTemplate.getForObject(restServiceUrlFind, Client.class);
  
  Assert.assertNotNull(clientFound);
  Assert.assertEquals(clientFound.getId(), clientCreated.getId());
 }
 
 @Test
 public void getAllTest() {
  
  Client newClient = createNewClient();
  restTemplate.postForObject(REST_SERVICE_URL, newClient, Client.class);
  
  Clients clients = (Clients) restTemplate.getForObject(REST_SERVICE_URL, Clients.class);
  
  Assert.assertNotNull(clients);
  Assert.assertNotNull(clients.getClients());
  Assert.assertTrue(clients.getClients().size() > 0);
 }
 
 @Test
 public void deleteTest() {
  
  Client newClient = createNewClient();
  Client clientCreated = restTemplate.postForObject(REST_SERVICE_URL, newClient, Client.class);
  
  Assert.assertNotNull(clientCreated);
  Assert.assertNotNull(clientCreated.getId());
  
  //It should be found
  String restServiceUrlFind = REST_SERVICE_URL + "/" + clientCreated.getId();
  Client clientFound = restTemplate.getForObject(restServiceUrlFind, Client.class);
  Assert.assertNotNull(clientFound);
  Assert.assertNotNull(clientFound.getId());
  
  restTemplate.delete(restServiceUrlFind);
  
  //It should not be found
  Client clientDeleted = restTemplate.getForObject(restServiceUrlFind, Client.class);
  Assert.assertNull(clientDeleted);
 }
 
}
This test is the so called integration test we created. You can run it executing mvn verify.

About @Controller @Repository and @Service annotations

All of them do the same! they tell Spring that the class annotated is a Spring bean. So, why don't we use something like "@Bean" for every bean? Because the idea goes like this:
  • Every DAO should be @Repository
  • Every Controller/Action should be @Controller
  • Every Service should be @Service
Say, for instance, you have all of your beans annotated with @Controller (remember those 3 are the same) and you want to apply transactions via aspects with Spring to every Service class. Then, if you had all of your service beans annotated with @Service you could choose to apply transaction via aspects to every class annotated with @Service.

Download the complete example!

Checkout the project: https://subversion.assembla.com/svn/pablo-examples/spring-webapp-example

Run mvn test -> To run the unit tests only
Run mvn verify -> To run the unit tests + integration tests
Run mvn verify -Dmaven.test.skip=true -> To run integration tests only

Additionaly, you may run mvn eclipse:eclipse to convert it to Eclipse project

Sunday, July 22, 2012

REST services with RESTEasy (XML, JSON)

In the previous post, I wrote about creating REST services (either JSON or XML) with Spring 3.1.1-Final. Now I will post about doing basically the same with RESTEasy which is another product, this time from JBoss.


Versions used:

- Maven 3
- RESTEasy 2.3.4.Final


pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>
 
 <groupId>ar.com.pabloExample</groupId>
 <artifactId>resteasy-example</artifactId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 <name>resteasy-example Maven Webapp</name>
 <url>http://maven.apache.org</url>
 
 <repositories>
  <repository>
   <id>jboss-releases</id>
   <name>JBoss Releases</name>
   <url>https://repository.jboss.org/nexus/content/repositories/releases/</url>
  </repository>
 </repositories>
 
 <dependencies>
 
  <!-- RESTEasy dependencies -->

  <dependency>
   <groupId>org.jboss.resteasy</groupId>
   <artifactId>resteasy-jaxrs</artifactId>
   <version>2.3.4.Final</version>
  </dependency>
  
  <dependency>
   <groupId>org.jboss.resteasy</groupId>
   <artifactId>resteasy-jaxb-provider</artifactId>
   <version>2.3.4.Final</version>
  </dependency>

  <dependency>
   <groupId>org.jboss.resteasy</groupId>
   <artifactId>resteasy-jettison-provider</artifactId>
   <version>2.3.4.Final</version>
  </dependency>

  <dependency>
   <groupId>org.jboss.resteasy</groupId>
   <artifactId>resteasy-multipart-provider</artifactId>
   <version>2.3.4.Final</version>
  </dependency>
  
  <!-- For testing purposes -->
 
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.2</version>
   <scope>test</scope>
  </dependency>
  
 </dependencies>
 
 <build>
  <finalName>resteasy-example</finalName>
  
  <plugins>
  
   <!-- With this you can start the server by doing mvn jetty:run -->
   <!-- Somehow, running this test via MVN JETTY:RUN will not work -->
   <!-- You will have to run it as MVN JETTY:RUN-WAR -->
   <plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.26</version>
    <configuration>
     <scanIntervalSeconds>3</scanIntervalSeconds>
     <systemProperties>
      <systemProperty>
       <name>log4j.configurationFile</name>
       <value>file:${project.basedir}/src/test/resources/log4j.properties</value>
      </systemProperty>
     </systemProperties>
    </configuration>
   </plugin>
  
   <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
     <source>1.6</source>
     <target>1.6</target>
    </configuration>
   </plugin>
   
  </plugins>
 </build>
</project>

web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
 <display-name>Archetype Created Web Application</display-name>
 
 <!-- Auto scan REST service -->
 
 <context-param>
  <param-name>resteasy.scan</param-name>
  <param-value>true</param-value>
 </context-param>
 
 <!-- this has to match with resteasy-servlet url-pattern -->
 
 <context-param>
  <param-name>resteasy.servlet.mapping.prefix</param-name>
  <param-value>/rest</param-value>
 </context-param>

 <!-- to return data according to extension -->
 
 <context-param>
  <param-name>resteasy.media.type.mappings</param-name>
  <param-value>json : application/json, xml : application/xml</param-value>
 </context-param>
 
 <listener>
  <listener-class>
   org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
  </listener-class>
 </listener>

 <servlet>
  <servlet-name>resteasy-servlet</servlet-name>
  <servlet-class>
   org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
  </servlet-class>
 </servlet>
 
 <servlet-mapping>
  <servlet-name>resteasy-servlet</servlet-name>
  <url-pattern>/rest/*</url-pattern>
 </servlet-mapping>

</web-app>

Every http://localhost:8080/resteasy-example/rest/* request is going to be read by the resteasy servlet

And we configured it to accept URL extension based services. This means that we create the service only once and if I make an URL request ending in .json it will return the data in JSON format, and if we send the request ending in .xml, it will return data in XML format.


Book.java

public class Book {

 private Integer id;
 private String name;
 

 public void setId(Integer id) {
  this.id = id;
 }

 @XmlAttribute
 public Integer getId() {
  return id;
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 @XmlElement
 public String getName() {
  return name;
 }

}

These annotations in Book and Books class are able to be understood by both XML and JSON parser.


Books.java

@XmlRootElement
public class Books {
 
 private List<Book> books;

 
 @XmlElement(name="book")
 public List<Book> getBooks() {
  return books;
 }

 public void setBooks(List<Book> books) {
  this.books = books;
 }
 
}

This class acts as a Book list wrapper. To create a REST service or webservice that returns List<Book> directly may not work in all environments. So I find the easiest way is to create a wrapper to mitigate this kind of problems.


BookService.java

@Path("/services")
public class BookService {
 
 @GET
 @Path("/books")
 @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
 public Books getBooksXml() {
  
  Books books = new Books();
  List<Book> bookList = returnData();
  books.setBooks(bookList);
  
  return books;
 }
 
 private List<Book> returnData() {
  
  ArrayList<Book> names = new ArrayList<Book>();
  
  Book b1 = new Book();
  Book b2 = new Book();
  Book b3 = new Book();
  
  b1.setName("book nro. 1");
  b1.setId(0);
  b2.setName("book nro. 2");
  b2.setId(1);
  b3.setName("book nro. 3");
  b3.setId(2);
  
  names.add(b1);
  names.add(b2);
  names.add(b3);
  
  return names;
 }

}

Here you can see that I only write the service once, and with the web.xml configuration I did and the @Produces annotation, we can either return JSON or XML depending on the URL extension sent.


Let's test the REST service!


Firstly, run the server from a terminal: mvn jetty:run-war -Dmaven.test.skip=true

You can access via a browser:

http://localhost:8080/resteasy-example/rest/services/books.xml
http://localhost:8080/resteasy-example/rest/services/books.json



Then you can run BookServiceTest that acts as a client service with mvn test


Note: somehow, the command mvn jetty:run won't expose correctly your services, that's why we run mvn jetty:run-war skipping tests (because the precondition of the tests is that the server is up and running)


BookServiceTest.java

public class BookServiceTest {
 
 @Test
 public void bookServiceTest() throws Exception {
  
  ClientRequest clientRequest = new ClientRequest("http://localhost:8080/resteasy-example/rest/services/books.xml");
//  ClientRequest clientRequest = new ClientRequest("http://localhost:8080/resteasy-example/rest/services/books.json");
  ClientResponse<Books> clientResponse = clientRequest.get(Books.class);
  
  Books books = clientResponse.getEntity();
  List<Book> bookList = books.getBooks();
  
  Assert.assertTrue(bookList.size() > 0);
 }

}

Download the complete example!


https://subversion.assembla.com/svn/pablo-examples/resteasy-example


Finally...


I prefer implementing REST services with RESTEasy rather that using the Spring solution I proposed in the previous post, unless you are already using a Spring stack on your app. Even if you are already using Spring, it may be worth taking a look at the RESTEasy-Spring integration solution which we are not trying in this example.

Friday, July 6, 2012

REST services with Spring 3 (XML, JSON)

As an alternative to webservices, we can use REST. Both REST and webservices have their differences. While in a previous post I wrote an example of webservices with Spring 3, I will post here one way of implementing REST using Spring 3. This example consists of a service that can return either XML or JSON depending on the URL invoked. You will be able to check the return values via a browser or a spring client test (which is provided in the example). The example also includes a web page built with extJS that fills a grid with data obtained via the service that return JSON data. Let's see it!


Versions used:
  • Spring 3.1.1-RELEASE
  • XStream 1.4.2 (XML parser)
  • Jackson 1.9.7 (JSON parser)
  • Maven 3


pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>ar.com.pabloExample</groupId>
 <artifactId>spring-rest-example</artifactId>
 <packaging>war</packaging>
 <version>1.0-SNAPSHOT</version>
 <name>spring-rest-example Maven Webapp</name>
 <url>http://maven.apache.org</url>

 <dependencies>

  <!-- Spring dependencies -->

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-web</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>
  
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-oxm</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>compile</scope>
  </dependency>

  <!-- JSON parser -->
  
  <dependency>
   <groupId>org.codehaus.jackson</groupId>
   <artifactId>jackson-mapper-asl</artifactId>
   <version>1.9.7</version>
   <scope>compile</scope>
  </dependency>

  <!-- XML parser -->
  
  <dependency>
   <groupId>com.thoughtworks.xstream</groupId>
   <artifactId>xstream</artifactId>
   <version>1.4.2</version>
   <scope>compile</scope>
  </dependency>

  <!-- For testing purposes -->

  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
   <scope>test</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>3.1.1.RELEASE</version>
   <scope>test</scope>
  </dependency>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.2</version>
   <scope>test</scope>
  </dependency>

 </dependencies>

 <build>
  <finalName>spring-rest-example</finalName>
  
  <plugins>
  
   <!-- With this you can start the server by doing mvn jetty:run -->
   <plugin>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.26</version>
    <configuration>
     <scanIntervalSeconds>3</scanIntervalSeconds>
    </configuration>
   </plugin>
  
   <plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
     <source>1.6</source>
     <target>1.6</target>
    </configuration>
   </plugin>
   
  </plugins>
 </build>
</project>

Book.java

@XStreamAlias("book")
public class Book {

 private Integer id;
 private String name;

 //Getters and setters ommited but must exist
}

We use the annotation @XStreamAlias("book") so that the XML generated output for the root book tag is <book>, else, it would be <ar.com.pabloExample.model.Book> because of the way XStream handles XML transformation.

<list>
 <book>
  <id>0</id>
  <name>book nro. 1</name>
 </book>
 <book>
  <id>1</id>
  <name>book nro. 2</name>
 </book>
 <book>
  <id>2</id>
  <name>book nro. 3</name>
 </book>
</list>

BookController.java

@Controller
@RequestMapping("/book")
public class BookController {

 @RequestMapping(value="/names", method=RequestMethod.GET)
 public List<Book> getNames() {
  
  return returnData();
 }
 
 private List<Book> returnData() {
  
  ArrayList<Book> names = new ArrayList<Book>();
  
  Book b1 = new Book();
  Book b2 = new Book();
  Book b3 = new Book();
  
  b1.setName("book nro. 1");
  b1.setId(0);
  b2.setName("book nro. 2");
  b2.setId(1);
  b3.setName("book nro. 3");
  b3.setId(2);
  
  names.add(b1);
  names.add(b2);
  names.add(b3);
  
  return names;
 }
}

With those @RequestMapping annotations we are able to create a REST service so that it returns the getNames() return value whenever this address is invoked:
http://localhost:8080/spring-rest-example/rest/book/names


Books.java

public class Books {
 
 @JsonProperty("bookList")
 private
 List<Book> books;

 public List<Book> getBooks() {
  return books;
 }

 public void setBooks(List<Book> books) {
  this.books = books;
 }
}

This entity is just a wrapper for our List<Book>, my JSON example wouldn't work managing the list directly.
Also, the @JsonProperty("bookList") annotation exists in order to map the return "bookList" to the "books" attribute.

{"bookList":[{"id":0,"name":"book nro. 1"},{"id":1,"name":"book nro. 2"},{"id":2,"name":"book nro. 3"}]}


web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
 <display-name>Archetype Created Web Application</display-name>

 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath*:applicationContext.xml</param-value>
 </context-param>

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

 <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>/rest/*</url-pattern>
 </servlet-mapping>

</web-app>

This is an usual Spring web configuration context. Every http://localhost:8080/spring-rest-example/rest/* request will be attended by the spring-servlet.xml


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:mvc="http://www.springframework.org/schema/mvc"
 xmlns:context="http://www.springframework.org/schema/context"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
  http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
  http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.1.xsd">

 <mvc:annotation-driven/>

 <context:component-scan base-package="ar.com.pabloExample.controller" />

 <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
  <property name="order" value="1" />
  <property name="mediaTypes">
   <map>
    <entry key="json" value="application/json" />
    <entry key="xml" value="application/xml" />
   </map>
  </property>

  <property name="defaultViews">
   <list>
    <!-- JSON View -->
    <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />

    <!-- XML View -->
    <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
     <constructor-arg>
      <bean class="org.springframework.oxm.xstream.XStreamMarshaller">
       <property name="autodetectAnnotations" value="true"/>
      </bean>
     </constructor-arg>
    </bean>
   </list>
  </property>
 </bean>

</beans>

Here, we configure ContentNegotiatingViewResolver to either accept XML and JSON when answering a request.
http://localhost:8080/spring-rest-example/rest/book/names - returns XML (default when no extension)
http://localhost:8080/spring-rest-example/rest/book/names.xml - returns XML
http://localhost:8080/spring-rest-example/rest/book/names.json - returns JSON


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"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

</beans>

This XML file is mandatory in the Spring configuration. We put nothing here.


Let's test the services!


test-beans.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"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

    <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">  
      <property name="messageConverters">
       <list>
           <bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
            <property name="marshaller" ref="xstreamMarshaller"/>
            <property name="unmarshaller" ref="xstreamMarshaller"/>
           </bean>
           <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
       </list>
      </property>
    </bean>
    
 <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
  <property name="aliases">
         <props>
             <prop key="book">ar.com.pabloExample.model.Book</prop>
         </props>
     </property>
 </bean>

</beans>

The RestTemplate class has configuration for both XML and JSON message converters.
For XML marshalling we use XStream, and configure an alias so it is able to understand the <book> refers to <ar.com.pabloExample.model.Book>


RestClientTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/test-beans.xml" })
public class RestClientTest {

 @Autowired
 @Qualifier("restTemplate")
 private RestTemplate restTemplate;
 
 @Test
 public void restJsonClientTest() {
  
  Books booksAux = restTemplate.getForObject("http://localhost:8080/spring-rest-example/rest/book/names.json", Books.class);
  List<Book> books = booksAux.getBooks();
  
  Assert.assertNotNull(books);
  Assert.assertTrue(books.size() > 0);
 }
 
 @SuppressWarnings("unchecked")
 @Test
 public void restXmlClientTest() {
  
  List<Book> books = (ArrayList<Book>) restTemplate.getForObject("http://localhost:8080/spring-rest-example/rest/book/names.xml", List.class);
  
  Assert.assertNotNull(books);
  Assert.assertTrue(books.size() > 0);
 }
}

Run mvn jetty:run to start jetty web server so that the webservice is exposed, then you can run RestClientTest doing mvn test.

http://localhost:8080/spring-rest-example/rest/book/names - returns XML (default when no extension)

http://localhost:8080/spring-rest-example/rest/book/names.xml - returns XML
http://localhost:8080/spring-rest-example/rest/book/names.json - returns JSON


You can access http://localhost:8080/spring-rest-example/example.html too to see an extJS data grid example that is filled with the JSON service we've just deployed.


Get the complete code!


https://subversion.assembla.com/svn/pablo-examples/spring-rest-example/

  1. First, run mvn jetty:run to start jetty web server
  2. Then mvn test to run JUnit tests


Finally


I think there's too much boilerplate configuration to enable REST services with Spring, unless you're already using Spring to build your app. Maybe other products like RESTEasy are more suited if we don't want to use Spring for our webapp but still want to have services exposed via REST...

For more information about mvc:annotation-driven tag, @Controller annotation, @RequestMapping see:
http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/mvc.html