Monday, 16 May 2011

Creating a Spring Web Service Using Version 2.0.1.RELEASE

I thought that I’d write a Web Service using the Spring Web Services product. This is described as a Contract-First web service; however, the guys at Spring have made life a little easier for us in that the product can dynamically create a WDSL file, which is a feature that we’ll be using.

The scenario for this example is of a wine search service, in which the client sends a request containing a country name and gets back a list of wines produced by that country. For the purposes of this example, I already have a business service POJO and that I’ll be injecting it into my web service code.

To create the web service, the following steps are required:

Step 1: Create your project using Maven

This is easily done using the following Maven command together with the accompanying Spring archetype. The following command should be on a single line:
mvn archetype:create -DarchetypeGroupId=org.springframework.ws 
  -DarchetypeArtifactId=spring-ws-archetype 
  -DarchetypeVersion=2.0.1.RELEASE 
  -DgroupId=com.mycompany 
  -DartifactId=my-tips-spring-webservice

Step 2: Create your XML Schema

For this, take a look at my blog on 14/05/2011, which describes how to create a wine search schema for this scenario. The schema is important and should be saved in the project’s webapp\WEB-INF directory with the name wine-search.xsd

Step 3 - Create the web.xml file

The next step is to sort out the web.xml file. This is pure boiler plate code and a skeletal outline is created by the Maven archetype. I prefer to change the servlet name to 'wine-search-ws', which fits the project (although this is optional) and also, because the we’re using Spring’s dynamically created WSDL file, the following lines should be added to the servlet config:

<init-param>
    <param-name>transformWsdlLocations</param-name>
    <param-value>true</param-value>
  </init-param>    

Remember to make a note of the servlet name: ‘wine-search-ws’ as it’s needed in the next step as the prefix to the name of the spring config file.

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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"
         version="2.4">

    <display-name>Wine Search Sample</display-name>

    <!-- take especial notice of the name of this servlet -->
    <servlet>
        <servlet-name>wine-search-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
  <init-param>
    <param-name>transformWsdlLocations</param-name>
    <param-value>true</param-value>
  </init-param>    
    </servlet>

    <servlet-mapping>
        <servlet-name>wine-search-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

Step 4 - Set up the Spring Config

The Maven archetype for this project provides you with a boiler plate version of your Spring config file, but you’ll need to rename it to: wine-search-ws-servlet.xml so that Spring’s servlet can access it. As for the business end of this file, it requires three entries each with a specific purpose. Entries are required to allow:
  1. the autowiring of the Spring Web Service and use of the Spring web service annotations. This is added by the Maven project archetype.
  2. dependency injection and autowiring of the underlying business service. You will need to add this entry.
  3. the dynamic generation of the WSDL. You will also need to add this entry.
The above are illustrated in the following sample 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:sws="http://www.springframework.org/schema/web-services"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

   <!-- Use this for autowired DI -->
   <context:component-scan base-package="marin.tips"/>

   <!-- Use this for your annotation driven end point -->
   <sws:annotation-driven/>

 <sws:dynamic-wsdl id="winesearch"                                                           
     portTypeName="WineSearch"                                                         
     locationUri="/winesearch/"                                                       
     targetNamespace="http://marin.tips.spring.webservice/schemas/">                               
    <sws:xsd location="/WEB-INF/wine-search.xsd"/>                                                  
 </sws:dynamic-wsdl>
</beans>

Step 5 - Create the End Point class

The next important step is to create the endpoint class. In Spring WS version 2.0.1.RELEASE, the old abstract classes that support the Template Pattern have been deprecated in favour of annotations. The interface to our endpoint class uses JDom, which is one of a range of options supported by Spring WS.

This example assumes that we already have a service class that implements our business case and the endpoint is solely responsible for unwrapping the incoming XML and creating the outgoing reply.

The endpoint class uses the following annotations:

@Endpoint

This is the annotation that defines the endpoint class as just that, an endpoint class.

@Endpoint
public class WineSearchEndpoint {

@PayloadRoot, @RequestPayload and @ResponsePayload

These annotations are used to mark any method as an end point method. In this case, the method is getWineByCountry(...). If the method does not return a response, you can omit the @ResponsePayload annotation.

  @PayloadRoot(namespace = NAMESPACE_URI, localPart = "WineSearch")
 
@ResponsePayload
 
public Element getWineByCountry(@RequestPayload Element wineSearch) throws Exception {

The @PayloadRoot annotation has two useful attributes: namespace and localPart . The namespace should be set to the namespace of the schema, whilst the localPart reflects the name of the first XML element that we’re interested in handling.

@Autowired

This is a core Spring annotation used to inject our existing service class into the endpoint class.

  @Autowired
 
public WineSearchEndpoint(WineService wineService) {

And, just in case you’re interested, the whole end point class is given below:

@Endpoint
public class WineSearchEndpoint {

 
private static final String NAMESPACE_URI = "http://marin.tips.spring.webservice/schemas";

 
private static Namespace ns = Namespace.getNamespace("wn", NAMESPACE_URI);

 
private final WineService wineService;

 
/**
   * 'Autowired' annotation injects the service into this WS facade
   */
 
@Autowired
 
public WineSearchEndpoint(WineService wineService) {

   
this.wineService = wineService;
 
}

 
/**
   * The 'PayloadRoot' annotation tells Spring-WS that the getWineByCountry method is
   * suitable for handling XML messages. The sort of message that this method can handle is
   * indicated by the annotation values, in this case, it can handle XML elements that have
   * the wineSearch local part and the our wine search namespace.
   */
 
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "WineSearch")
 
@ResponsePayload
 
public Element getWineByCountry(@RequestPayload Element wineSearch) throws Exception {

   
String country = resolveCountry(wineSearch);
    WineList wines = wineService.getWineByCountry
(WineCountry.valueOf(country));

   
return createReturnXML(wines, country);
 
}

 
private String resolveCountry(Element wineSearch) {

   
Element request = wineSearch.getChild("Request", ns);
   
return request.getChildText("Country", ns);
 
}

 
private Element createReturnXML(WineList wines, String country) {

   
Element wineSearch = new Element("WineSearch", ns);
    Element result = createResultElement
(country);
    wineSearch.addContent
(result);

    addWineList
(wines, result);
   
return wineSearch;
 
}

 
private Element createResultElement(String country) {

   
Element result = new Element("Response", ns);
    result.setAttribute
("country", country);
   
return result;
 
}

 
private void addWineList(WineList wines, Element result) {

   
for (Wine wine : wines) {
     
Element wineElement = new Element("Wine", ns);
      addWineInfo
(wine, wineElement);
      result.addContent
(wineElement);
   
}
  }

 
private void addWineInfo(Wine wine, Element wineElement) {

   
wineElement.addContent(createElement("Name", wine.getName()));
    wineElement.addContent
(createElement("Origin", wine.getOrgin()));
    wineElement.addContent
(createElement("Strength", Double.toString(wine.getStrength())));
    wineElement.addContent
(createElement("Colour", wine.getColour().toString()));
 
}

 
private Element createElement(String elementName, String elementValue) {

   
Element element = new Element(elementName, ns);
    element.setText
(elementValue);
   
return element;
 
}

}

Testing

The above code was rigorously tested using JUnit and Easymock. Once deployed, it was tested using JUnit and Spring’s WebServiceTemplate class, a useful piece of code in the mould of JmsTemplate and JdbcTemplate. The code snippet below gives a useful insight into using this class:

    Document document = createInputDoc(TEST_FILE_PATH + "france.xml");
    JDOMResult result =
new JDOMResult();

    WebServiceTemplate wsTemplate =
new WebServiceTemplate();

    String url =
"http://localhost:8080/marin-tips-spring-webservice/winesearch";
    wsTemplate.sendSourceAndReceiveToResult
(url,
       
new JDOMSource(document.getRootElement()), result);

    Document doc = result.getDocument
();

From the sample above, you may have guessed that the web service can be accessed (on my tomcat server) on the following URL:

http://localhost:8080/marin-tips-spring-webservice/winesearch

I mentioned above that we’re creating the WDSL dynamically, so if you need access to this file, then it can be downloaded from your server using the following URL:

http://localhost:8080/marin-tips-spring-webservice/winesearch.wsdl

Additional information can be found in Spring’s own tutorial, but it doesn’t cover returning data to the client code - something of an oversight to get you thinking?

2 comments:

Anonymous said...

Nice example! But it would be even nicer if the complete code were available. Now there are several methods missing, as far as I can see.

And why not base it on the Spring WS Test framework? Would make it evene easier to test.

Roger Hughes said...

Thanks for your comment. The blog is not really supposed to be a source code repository (although I have been toying with that idea), but a place where I can jot down techniques and opinions.

For the sake of clarity, when writing a blog I only include source code that's relevant to what I'm talking about. In this case, the WineService has not been listed because it is just an interface to any other service component and it's not part of 'how to' write a Web Service. Whereas, the WineSearchEndpoint has been included in full as it's crucial to what I'm talking about.

I could have tried to re-use the Spring WS example, but created my own project from scratch using their Maven archetype.