Saturday, September 19, 2009

RESTful Web Services with Spring 3 Experience

Arjen Poutsma introduced REST support in Spring 3 through his two blogs REST in Spring 3: @MVC and REST in Spring 3: RestTemplate. Follow his instructions and Spring 3 PetClinic sample application, I got RESTful Web Service working in my Open Toast Project.

Just to provide a very simple service "get member object by member id", I created a new Web bundle project "org.opentoast.rest" by copying "org.opentoast.web" project. There is only one Java class in new project, MemebrController:

@Controller
public class MemberController
{
    protected final Log log = LogFactory.getLog(getClass());
    
    private MemberManager memberManager;
    
    @Autowired
    public MemberController(MemberManager memberManager){
        this.memberManager = memberManager;
    }
    
    @RequestMapping(value="/members/{memberId}", method = RequestMethod.GET)
    public ModelAndView getMember(@PathVariable("memberId") Long id) {
        Member m = memberManager.getMemberById(id);
        ModelAndView mav = new ModelAndView("member");
        mav.addObject("member", m);
        return mav;
    }
}

It provides one RESTful service in the method getMember(). It is annotated by @RequestMapping with URI template /members/{memberId}. Using @PathVariable("memberId"), the request member id is passed to parameter id. So with the injected MemberManager, we can get the Member object by id and pass to returned ModelAndView object. The view name is "member", and result member object is put into model with key "member" as well.

To setup REST support, there are few changes in config file module-context.xml:

<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:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:oxm="http://www.springframework.org/schema/oxm"
  xsi:schemaLocation="
   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
   http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
   http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">

 <context:component-scan base-package="org.opentoast.rest.controller"/>

 <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
   
 <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
 
 <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
 
 <bean id="member" class="org.springframework.web.servlet.view.xml.MarshallingView">
  <property name="contentType" value="application/vnd.opentoast.rest+xml"/>
  <property name="marshaller" ref="marshaller"/>
  <property name="modelKey" value="member"/>
 </bean>

 <oxm:jaxb2-marshaller id="marshaller">
  <oxm:class-to-be-bound name="org.opentoast.domain.Member"/>
 </oxm:jaxb2-marshaller>

</beans>

Since we only want to support REST service with customized content type, we don’t need ContentNegotiatingViewResolver , so only one view resolver is set in the context: org.springframework.web.servlet.view.BeanNameViewResolver. It will resolve view named "member" to bean "member", which is an instance of org.springframework.web.servlet.view.xml.MarshallingView. The content type is "application/vnd.opentoast.rest+xml", modelKey is "member" and reference to jaxb2 marshaller bean "marshaller".

The web.xml sets servlet with org.springframework.web.servlet.DispatcherServlet, named opentoastrest, and maps to /*:

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 version="2.5">

 <display-name>Open Toast RESTful</display-name>

 <context-param>
  <param-name>contextClass</param-name>
  <param-value>com.springsource.server.web.dm.ServerOsgiBundleXmlWebApplicationContext</param-value>
 </context-param>
 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>
   /META-INF/spring/module-context.xml
   /META-INF/spring/osgi-context.xml
  </param-value>
 </context-param>

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

 <servlet>
  <servlet-name>opentoastrest</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
   <param-name>contextConfigLocation</param-name>
   <param-value></param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
 </servlet>

 <servlet-mapping>
  <servlet-name>opentoastrest</servlet-name>
  <url-pattern>/*</url-pattern>
 </servlet-mapping>
</web-app>

You can check out the code from Google Code by running

svn checkout http://opentoastproject.googlecode.com/svn/tags/opentoast_20090919 opentoast

Then inside opentoast folder, run

mvn package

to build the whole application. Copy the par file at opentoast/org.opentoast.par/target/OpenToast.par to springsource dm Server pickup folder, and start dm Server.

To test the RESTful Web Service, here is a client example code:

package org.opentoast.client;

import java.util.HashMap;
import java.util.Map;

import org.opentoast.domain.Member;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.web.client.RestTemplate;

public class RestClient
{
    RestTemplate restTemplate;
    
    public RestClient(RestTemplate template)
    {
        this.restTemplate = template;
    }
    
    public void testMember()
    {
        Map<String, String> vars = new HashMap<String, String>();
        vars.put("memberId", "1");
        Member result = restTemplate.getForObject(
                "http://localhost:8080/opentoastrest/members/{memberId}",
                Member.class, vars);
        System.out.println(result);
    }
    
    public static void main(String[] args)
    {
        ApplicationContext ac = new ClassPathXmlApplicationContext(
                "/org/opentoast/client/appContext.xml");
        RestClient client = (RestClient)ac.getBean("restClient");
        client.testMember();
    }
}
With xml config file /org/opentoast/client/appContext.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:oxm="http://www.springframework.org/schema/oxm"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">

 <bean id="restClient" class="org.opentoast.client.RestClient">
  <constructor-arg ref="restTemplate"/>
 </bean>

 <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
  <property name="messageConverters">
   <list>
    <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
     <constructor-arg ref="marshaller"/>
     <property name="supportedMediaTypes">
      <list>
       <bean class="org.springframework.http.MediaType">
        <constructor-arg value="application"/>
        <constructor-arg value="vnd.opentoast.rest+xml"/>
       </bean>
      </list>
     </property>
    </bean>
   </list>
  </property>
 </bean>

 <oxm:jaxb2-marshaller id="marshaller">
  <oxm:class-to-be-bound name="org.opentoast.domain.Member"/>
 </oxm:jaxb2-marshaller>
 
</beans>
Run this test client program and you will get print out:

[Member 1: Jane Smith]

One big question I have now is "Do I need to expose my rich domain model as REST object model?"

JAXB2 marshaller requires Member class to be annotated with @XmlRootElement. And my domain model class Member is already annotated with persistent API, so now it looks like:

@XmlRootElement
@Entity
@Table(name = "MEMBER")
public class Member extends BaseEntity
{
...

If I don't want REST client to see my rich domain model, I have to use DTO model, and map between those two models. That will defeat the purpose of Domain Driven Design. If I expose my rich domain model classes to client, it will be more problematic when I try to use GWT as client application running inside browser. So far I haven't found a good solution for GWT + REST.

Monday, September 14, 2009

Release s2dm Maven Plugin 0.0.3

Open Toast Project Maven Plugin for Spring Source dm Server Development 0.0.3 was released.

After Spring Bundlor released 1.0M5, I tried and got it working with my project.

Still have trouble with M2Eclipse, the Maven Dependencies classpath of web project doesn't include depending projects in workspace (domain and service projects). So I have to manually add those two projects to web project Java Build Path.

The 0.0.3 release can be used to set up Eclipse workspace, and build par.

To use that plugin, in parent pom.xml, setup plugin repository to

 <pluginRepositories>
  <!-- Maven 2 Plugin for S2DM -->
  <pluginRepository>
   <id>opentoast.maven.s2dm.plugin</id>
   <url>http://opentoastproject.googlecode.com/svn/mavenrepo</url>
  </pluginRepository>
 
  <!-- SpringSource milestone -->
  <pluginRepository>
   <id>com.springsource.repository.bundles.milestone</id>
   <name>SpringSource Enterprise Bundle Repository</name>
   <url>http://repository.springsource.com/maven/bundles/milestone</url>
  </pluginRepository>

 </pluginRepositories>

So go to project root directory /opentoast, and type

mvn org.opentoast:maven-s2dm-plugin:0.0.3:eclipse

It will generate all eclipse setting files, such as /project, .classpath, .settings/org.eclipse.wst.common.project.facet.core.xml, etc. It also generates a dummy MANIFEST.MF file at generated/META-INF/MANIFEST.MF under each bundle project folder. The trick is the .classpath will include the generated folder as classpathentry, so STS bundlor will automatically update it with template.mf settings.

To build the whole application into a par, just type mvn package under opentoast, it will use dm-par goal to package par artifact.

So far STS does not work with dm Server 2.0.0.M4, so I just use STS to edit code, and use command line to build par, then deploy to local running dm Server pickup folder. I will try to get the application running inside STS later. Hope dm Server and Spring 3 can be released before Christmas, so I can play with them during winter break.