Webservice with Spring Web Services 2.0 (M3)

Some time ago I had to define a web service and write the web service implementation for a spring 3.0 project. The operations needed were quite simple.

In the past I have usually used axis 1 to offer web services. The results have not been very satisfying because the development has always been quite complex and the generated wsdl was not very clean. In a perfect developer world I would expect to write an easy configuration defining which existing services should be accessible and doing a convention overconfiguration for the mapping of the parameters. This configuration could be an xml configuration file or annotations at the java class. Something similar to the spring REST support would be nice.

Because spring was already used in the project and spring is usually simple and powerful, I decided to try spring web services. Also testing the newest spring technology is for my company OPITZ-CONSULTING, which is a spring partner, important 😉

For this example I chose:
– a method returning a version information string
– a method with a string and a date parameter returning a boolean

These really are basical needs for a web service.

After some research I found out that spring web services 2.0, which was at M3 and is now released, is the right version to be combined with spring 3.0.

First we define the xsd or the operation request and responses:

1 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 <xs:schema xmlns:testService="http://ws.opitz-consulting.com/test/sws2/TestService" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://ws.opitz-consulting.com/test/sws2/TestService"> 3 <xs:element name="checkValueRequest"> 4 <xs:complexType> 5 <xs:sequence> 6 <xs:element name="testToken" type="xs:token"/> 7 <xs:element name="date" type="xs:date"/> 8 </xs:sequence> 9 </xs:complexType> 10 </xs:element> 11 <xs:element name="checkValueResponse"> 12 <xs:complexType> 13 <xs:sequence> 14 <xs:element name="checkValueReturn" type="xs:boolean"/> 15 </xs:sequence> 16 </xs:complexType> 17 </xs:element> 18 <xs:element name="getVersionRequest"/> 19 <xs:element name="getVersionResponse"> 20 <xs:complexType> 21 <xs:sequence> 22 <xs:element name="getVersionReturn" type="xs:token"/> 23 </xs:sequence> 24 </xs:complexType> 25 </xs:element> 26 </xs:schema>

The java methods of the spring bean containing the service has to be annotated. 

1 @Endpoint 2 @Namespace(prefix = "t", uri = "http://ws.opitz-consulting.com/test/sws2/TestService") 3 public class TestServiceEndpoint { 4 @TestService testService = null; 5 @PayloadRoot(localPart = "checkValueRequest", namespace = "http://ws.opitz-consulting.com/test/sws2/TestService") 6 @ResponsePayload 7 public boolean checkValue( 8 final @XPathParam("/t:checkValueRequest/t:testToken") String testToken, 9 final @XPathParam("/t:checkValueRequest/t:date") Date date) { 10 return this.testService.checkValue(testToken, date); 11 } 12 13 @PayloadRoot(localPart = "getVersionRequest", namespace = "http://ws.opitz-consulting.com/test/sws2/TestService") 14 @ResponsePayload 15 public String getVersion() { 16 return "$Revision: 1.0 $"; 17 } 18 }

This is the version I would like to look my java class like.
– The Endpoint annotation defines that the java class is a web service endpoint.
– The Namespace annotation defines the prefix t assigned to the given namespace.
– The PayloadRoot defines the name of the request part of the soap message and the namespace.
– With the XPathParam annotations are defined how the parameters are extracted from the received soap xml message. The parameter is an XPath-expression selecting the parameter.
– The ResponesPayload annotation tells spring web services to handle the response as a web service response message.

At the moment the documentation is very modest, most of the information was gained by reverse engineering the implementation. But I’m sure this will change in future.

The common spring webservice configuration looks like this:

1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xmlns:p="http://www.springframework.org/schema/p" 6 xmlns:oxm="http://www.springframework.org/schema/oxm" 7 xsi:schemaLocation="http://www.springframework.org/schema/beans 8 http://www.springframework.org/schema/beans/spring-beans.xsd 9 http://www.springframework.org/schema/context 10 http://www.springframework.org/schema/context/spring-context.xsd"> 11 12 <bean class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping" /> 13 14 <bean class="org.springframework.ws.server.endpoint.adapter.DefaultMethodEndpointAdapter"> 15 <property name="methodArgumentResolvers"> 16 <list> 17 <bean class="org.springframework.ws.server.endpoint.adapter.method.XPathParamMethodArgumentResolver" > 18 <property name="conversionService" ref="conversionService" /> 19 </bean> 20 </list> 21 </property> 22 <property name="methodReturnValueHandlers"> 23 <list> 24 <bean class="com.oc.ws.utils.SimplePayloadMethodReturnValueHandler" > 25 <property name="conversionService" ref="conversionService" /> 26 </bean> 27 </list> 28 </property> 29 </bean> 30 31 <bean id="conversionService" class="com.oc.ws.utils.XMLConversionServiceFactory" factory-method="createXMLConversionService" /> 32 33 <!-- Dont't use JBoss implementation, looses the return value --> 34 <bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"> 35 <property name="messageFactory"> 36 <bean class="com.sun.xml.internal.messaging.saaj.soap.ver1_1.SOAPMessageFactory1_1Impl"/> 37 </property> 38 </bean> 39 40 </beans>

– We use annotations to define the endpoints
– The mapping of the method parameters is done by XPath-Expressions
– The conversions of the xml datatypes to the java world is done by a conversion service. More about this later.
– The mapping of the return values is done by a default mapping handler using the conversion service. More about this later.
– The example is deployed on JBoss but by using the JBoss message factory implementation causes a loss of the return value during the web service call.

The spring configuration for our TestService:

1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xmlns:p="http://www.springframework.org/schema/p" 6 xmlns:oxm="http://www.springframework.org/schema/oxm" 7 xsi:schemaLocation="http://www.springframework.org/schema/beans 8 http://www.springframework.org/schema/beans/spring-beans.xsd 9 http://www.springframework.org/schema/context 10 http://www.springframework.org/schema/context/spring-context.xsd"> 11 12 13 <bean id="testServiceWS" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition"> 14 <property name="schema" ref="testServiceSchema"/> 15 <property name="portTypeName" value="SenderService"/> 16 <property name="locationUri" value="http://localhost:8888/test/service/testServiceWS"/> 17 <property name="targetNamespace" value="http://ws.opitz-consulting.com/test/sws2/TestService"/> 18 </bean> 19 20 <bean id="testServiceSchema" class="org.springframework.xml.xsd.SimpleXsdSchema"> 21 <property name="xsd" value="classpath:/de/oc/test/sws2/ws/server/testService.xsd"/> 22 </bean> 23 24 <bean id="testServiceEndpoint" class="de.oc.test.sws2.ws.server.TestServiceEndpoint"> 25 <constructor-arg ref="testService"/> 26 </bean> 27 28 <bean id="testServiceValidator" class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor"> 29 <property name="schema" value="classpath:/de/oc/test/sws2/ws/server/testService.xsd"/> 30 <property name="validateRequest" value="true"/> 31 <property name="validateResponse" value="false"/> 32 </bean> 33 34 </beans>

Here is nothing special.

Now to the two custom generic implementations. I don’t know why such an implementation is not already provided with spring (or in case it is provided, I haven’t found it yet). They make the implementation and the usage much easier.

The XMLConversionServiceFactory provides a service converting java types to default xml types. These are the default spring converters plus additional converters for java.util.Date and java.util.Calendar.

 

1 package com.oc.ws.utils; 2 3 import java.text.DateFormat; 4 import java.text.ParseException; 5 import java.text.SimpleDateFormat; 6 import java.util.Calendar; 7 import java.util.Date; 8 import java.util.GregorianCalendar; 9 10 import org.springframework.core.convert.converter.Converter; 11 import org.springframework.core.convert.support.ConversionServiceFactory; 12 import org.springframework.core.convert.support.GenericConversionService; 13 14 /** 15 * {@link ConversionServiceFactory} with additional {@link Converter}s for creating xml documents. 16 * 17 * @author bma (Opitz Consulting) 18 * @version $Revision: 1.8 $ 19 * @date 14.10.2010 20 */ 21 22 public class XMLConversionServiceFactory extends ConversionServiceFactory 23 { 24 /** sVersionId of the class XMLConversionServiceFactory. */ 25 public static final String sVersionId = "$Revision: 1.8 $"; 26 27 /** Log for output of messages of the class XMLConversionServiceFactory */ 28 // private static Log log = LogFactory.getLog(XMLConversionServiceFactory.class); 29 30 private static ThreadLocal<DateFormat> formatDate = new ThreadLocal<DateFormat>(); 31 32 private static ThreadLocal<DateFormat> formatDateTimezone = new ThreadLocal<DateFormat>(); 33 34 private static ThreadLocal<DateFormat> formatDateTime = new ThreadLocal<DateFormat>(); 35 36 private static ThreadLocal<DateFormat> formatDateTimeTimezone = new ThreadLocal<DateFormat>(); 37 38 /** 39 * Returns the value of the field 'formatDate'. 40 * 41 * @return Value of the field 'formatDate' 42 */ 43 private static DateFormat getFormatDate() 44 { 45 DateFormat result = XMLConversionServiceFactory.formatDate.get(); 46 if (result == null) 47 { 48 result = new SimpleDateFormat("yyyy-MM-dd"); 49 XMLConversionServiceFactory.formatDate.set(result); 50 } 51 return result; 52 } 53 54 /** 55 * Returns the value of the field 'getFormatDateTimezone'. 56 * 57 * @return Value of the field 'getFormatDateTimezone' 58 */ 59 private static DateFormat getFormatDateTimezone() 60 { 61 DateFormat result = XMLConversionServiceFactory.formatDateTimezone.get(); 62 if (result == null) 63 { 64 result = new SimpleDateFormat("yyyy-MM-ddZ"); 65 XMLConversionServiceFactory.formatDateTimezone.set(result); 66 } 67 return result; 68 } 69 70 /** 71 * Returns the value of the field 'formatDateTime'. 72 * 73 * @return Value of the field 'formatDateTime' 74 */ 75 private static DateFormat getFormatDateTime() 76 { 77 DateFormat result = XMLConversionServiceFactory.formatDateTime.get(); 78 if (result == null) 79 { 80 result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 81 XMLConversionServiceFactory.formatDateTime.set(result); 82 } 83 return result; 84 } 85 86 /** 87 * Returns the value of the field 'formatDateTimeTimezone'. 88 * 89 * @return Value of the field 'formatDateTimeTimezone' 90 */ 91 private static DateFormat getFormatDateTimeTimezone() 92 { 93 DateFormat result = XMLConversionServiceFactory.formatDateTimeTimezone.get(); 94 if (result == null) 95 { 96 result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 97 XMLConversionServiceFactory.formatDateTimeTimezone.set(result); 98 } 99 return result; 100 } 101 102 /** 103 * Converter for converting Calendar to xs:date and xs:dateTime strings. 104 * 105 * @author Bernhard Mähr (Opitz Consulting) 106 * @version $Revision: 1.8 $ 107 * @date 15.10.2010 108 */ 109 private static class CalendarToStringConverter implements Converter<Calendar, String> 110 { 111 112 @Override 113 public String convert(final Calendar source) 114 { 115 String result = null; 116 if (source.isSet(Calendar.HOUR)) 117 { 118 result = XMLConversionServiceFactory.getFormatDateTimeTimezone().format(source); 119 } 120 else 121 { 122 result = XMLConversionServiceFactory.getFormatDateTimezone().format(source); 123 } 124 return result; 125 } 126 127 } 128 129 /** 130 * Converter for converting xs:date and xs:dateTime strings to Calendar objects. 131 * 132 * @author Bernhard Mähr (Opitz Consulting) 133 * @version $Revision: 1.8 $ 134 * @date 15.10.2010 135 */ 136 private static class StringToCalendarConverter implements Converter<String, Calendar> 137 { 138 139 @Override 140 public Calendar convert(final String source) 141 { 142 Calendar result = null; 143 if ((source != null) && (source.trim().length() > 0)) 144 { 145 try 146 { 147 String trimedSource = source.trim(); 148 if (trimedSource.startsWith("-")) 149 { 150 trimedSource = trimedSource.substring(1); 151 } 152 Date date = null; 153 if (trimedSource.length() == 10) 154 { 155 date = XMLConversionServiceFactory.getFormatDate().parse(trimedSource); 156 } 157 else if (trimedSource.length() == 17) 158 { 159 date = XMLConversionServiceFactory.getFormatDateTimezone().parse(trimedSource); 160 } 161 else if (trimedSource.length() == 19) 162 { 163 date = XMLConversionServiceFactory.getFormatDateTime().parse(trimedSource); 164 } 165 else 166 { 167 date = XMLConversionServiceFactory.getFormatDateTimeTimezone().parse(trimedSource); 168 } 169 result = new GregorianCalendar(); 170 result.setTime(date); 171 } 172 catch (final ParseException pex) 173 { 174 throw new IllegalArgumentException( 175 "Can only convert a [String] with format [-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm] or [-]CCYY-MM-DD[Z|(+|-)hh:mm] to a [date]; string value '" 176 + source + "' has unknown format."); 177 } 178 } 179 return result; 180 } 181 182 } 183 184 /** 185 * {@inheritDoc} 186 */ 187 public static GenericConversionService createXMLConversionService() 188 { 189 final GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); 190 conversionService.addConverter(new XMLConversionServiceFactory.CalendarToStringConverter()); 191 conversionService.addConverter(new Converter<Date, String>() 192 { 193 CalendarToStringConverter internalConverter = new XMLConversionServiceFactory.CalendarToStringConverter(); 194 195 @Override 196 public String convert(final Date source) 197 { 198 final Calendar cal = new GregorianCalendar(); 199 cal.setTime(source); 200 return this.internalConverter.convert(cal); 201 } 202 }); 203 conversionService.addConverter(new StringToCalendarConverter()); 204 conversionService.addConverter(new Converter<String, Date>() 205 { 206 StringToCalendarConverter internalConverter = new StringToCalendarConverter(); 207 208 @Override 209 public Date convert(final String source) 210 { 211 return this.internalConverter.convert(source).getTime(); 212 } 213 }); 214 return conversionService; 215 } 216 217 }

 

The SimplePayloadMethodReturnValueHandler is a generic MethodProcessor converting the return value of the java method call to the web service call payload.

 

1 package com.oc.ws.utils; 2 3 import javax.xml.parsers.DocumentBuilder; 4 import javax.xml.parsers.DocumentBuilderFactory; 5 import javax.xml.transform.Source; 6 import javax.xml.transform.dom.DOMSource; 7 8 import org.springframework.core.MethodParameter; 9 import org.springframework.core.convert.ConversionService; 10 import org.springframework.core.convert.support.ConversionServiceFactory; 11 import org.springframework.ws.server.endpoint.adapter.method.AbstractPayloadSourceMethodProcessor; 12 import org.springframework.ws.server.endpoint.annotation.Namespace; 13 import org.springframework.ws.server.endpoint.annotation.PayloadRoot; 14 import org.w3c.dom.Document; 15 import org.w3c.dom.Element; 16 import org.w3c.dom.Node; 17 import org.w3c.dom.NodeList; 18 19 /** 20 * A MethodProcessor converting return values. If the Return value is one of the types 21 * <ul> 22 * <li>boolean or Boolean</li> 23 * <li>double or Double</li> 24 * <li>Node</li> 25 * <li>NodeList</li> 26 * <li>String</li> 27 * </ul> 28 * the processer converts the value to the source. For other types the conversionService (default {@link 29 * ConversionServiceFactory.createDefaultConversionService()}) is used. Example of configuration: <code> 30 * <bean class="org.springframework.ws.server.endpoint.adapter.DefaultMethodEndpointAdapter"> 31 * <property name="methodArgumentResolvers"> 32 * <list> 33 * <bean class="org.springframework.ws.server.endpoint.adapter.method.XPathParamMethodArgumentResolver" > 34 * <property name="conversionService" ref="conversionService" /> 35 * </bean> 36 * </list> 37 * </property> 38 * <property name="methodReturnValueHandlers"> 39 * <list> 40 * <bean class="com.oc.ws.utils.SimplePayloadMethodReturnValueHandler" > 41 * <property name="conversionService" ref="conversionService" /> 42 * </bean> 43 * </list> 44 * </property> 45 * </bean> 46 * </code> 47 * 48 * @author bma (Opitz Consulting) 49 * @version $Revision: 1.8 $ 50 * @date 08.10.2010 51 */ 52 53 public class SimplePayloadMethodReturnValueHandler extends AbstractPayloadSourceMethodProcessor 54 { 55 56 /** sVersionId of the class SimplePayloadMethodReturnValueHandler. */ 57 public static final String sVersionId = "$Revision: 1.8 $"; 58 59 /** Log for output of messages of the class SimplePayloadMethodReturnValueHandler */ 60 // private static Log log = LogFactory.getLog(SimplePayloadMethodReturnValueHandler.class); 61 62 /** The conversion service to use. */ 63 private ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); 64 65 /** 66 * Sets the value of the field 'conversionService' to the given value. 67 * 68 * @param conversionService 69 * Value the field 'conversionService' should get 70 */ 71 public void setConversionService(final ConversionService conversionService) 72 { 73 this.conversionService = conversionService; 74 } 75 76 /** 77 * {@inheritDoc} 78 * 79 * @see org.springframework.ws.server.endpoint.adapter.method.AbstractPayloadMethodProcessor#supportsResponsePayloadReturnType(org.springframework.core.MethodParameter) 80 */ 81 @Override 82 protected boolean supportsResponsePayloadReturnType(final MethodParameter methodparameter) 83 { 84 boolean result = false; 85 final Class<?> parameterType = methodparameter.getParameterType(); 86 if (Boolean.class.equals(parameterType) || Boolean.TYPE.equals(parameterType) 87 || Double.class.equals(parameterType) || Double.TYPE.equals(parameterType) 88 || Node.class.isAssignableFrom(parameterType) || NodeList.class.isAssignableFrom(parameterType) 89 || String.class.isAssignableFrom(parameterType)) 90 { 91 result = true; 92 } 93 else 94 { 95 result = this.conversionService.canConvert(parameterType, String.class); 96 } 97 return result; 98 } 99 100 /** 101 * {@inheritDoc} 102 * 103 * @see org.springframework.ws.server.endpoint.adapter.method.AbstractPayloadSourceMethodProcessor#createResponsePayload(org.springframework.core.MethodParameter, 104 * java.lang.Object) 105 */ 106 @SuppressWarnings("unchecked") 107 @Override 108 protected Source createResponsePayload(final MethodParameter methodparameter, final Object obj) throws Exception 109 { 110 final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 111 final Document doc = builder.newDocument(); 112 Namespace namespace = methodparameter 113 .getParameterAnnotation(org.springframework.ws.server.endpoint.annotation.Namespace.class); 114 if (namespace == null) 115 { 116 namespace = methodparameter 117 .getMethodAnnotation(org.springframework.ws.server.endpoint.annotation.Namespace.class); 118 } 119 if (namespace == null) 120 { 121 namespace = (Namespace)methodparameter.getDeclaringClass().getAnnotation( 122 org.springframework.ws.server.endpoint.annotation.Namespace.class); 123 } 124 if (namespace == null) 125 { 126 namespace = methodparameter.getDeclaringClass().getPackage().getAnnotation( 127 org.springframework.ws.server.endpoint.annotation.Namespace.class); 128 } 129 String methodName = methodparameter.getMethod().getName(); 130 final PayloadRoot payloadRootAnnotation = methodparameter 131 .getMethodAnnotation(org.springframework.ws.server.endpoint.annotation.PayloadRoot.class); 132 if (payloadRootAnnotation != null) 133 { 134 final String requestLocalName = payloadRootAnnotation.localPart(); 135 if ((requestLocalName != null) && requestLocalName.endsWith("Request")) 136 { 137 methodName = requestLocalName.substring(0, requestLocalName.length() - 7); 138 } 139 } 140 Element responseElement = null; 141 Element returnElement = null; 142 if (namespace != null) 143 { 144 responseElement = doc.createElementNS(namespace.uri(), methodName + "Response"); 145 returnElement = doc.createElementNS(namespace.uri(), methodName + "Return"); 146 } 147 else 148 { 149 responseElement = doc.createElement(methodName + "Response"); 150 returnElement = doc.createElement(methodName + "Return"); 151 } 152 String value = null; 153 final Class parameterType = methodparameter.getParameterType(); 154 if (Node.class.equals(parameterType)) 155 { 156 final Node node = (Node)obj; 157 returnElement.appendChild(node); 158 } 159 else if (NodeList.class.equals(parameterType)) 160 { 161 final NodeList nodeList = (NodeList)obj; 162 for (int i = 0; i < nodeList.getLength(); i++) 163 { 164 final Node node = nodeList.item(i); 165 returnElement.appendChild(node); 166 } 167 } 168 else 169 { 170 if (Boolean.class.equals(parameterType) || Boolean.TYPE.equals(parameterType) 171 || Double.class.equals(parameterType) || Double.TYPE.equals(parameterType) 172 || String.class.isAssignableFrom(parameterType)) 173 { 174 value = obj.toString(); 175 } 176 else 177 { 178 value = this.conversionService.convert(obj, String.class); 179 } 180 returnElement.setTextContent(value); 181 } 182 responseElement.appendChild(returnElement); 183 doc.appendChild(responseElement); 184 185 return new DOMSource(doc); 186 } 187 188 /** 189 * Always throws an UnsupportedOperationException. {@inheritDoc} 190 * 191 * @see org.springframework.ws.server.endpoint.adapter.method.AbstractPayloadSourceMethodProcessor#resolveRequestPayloadArgument(org.springframework.core.MethodParameter, 192 * javax.xml.transform.Source) 193 */ 194 @Override 195 protected Object resolveRequestPayloadArgument(final MethodParameter methodparameter, final Source source) 196 throws Exception 197 { 198 throw new UnsupportedOperationException("Class handles only return values."); 199 } 200 201 /** 202 * Always returns false. {@inheritDoc} 203 * 204 * @see org.springframework.ws.server.endpoint.adapter.method.AbstractPayloadMethodProcessor#supportsRequestPayloadParameter(org.springframework.core.MethodParameter) 205 */ 206 @Override 207 protected boolean supportsRequestPayloadParameter(final MethodParameter methodparameter) 208 { 209 return false; 210 } 211 212 }

Conclusion:
Spring-Web-Services doesn’t work as good out of the box I expected. With some generic classes it is possible to deploy basic web services only by doing some configuration. Perhaps these classes are added in a future version of spring web services.

Leave a Reply