Java and Spring development

Posts Tagged ‘exception handling

Spring WS client

with 5 comments

Introduction

The Spring WS client is a lightweight alternative that doesn’t need a WSDL to work. You code against a template like Spring’s other templates for communicating against a database or JMS server. This blog article demonstrates how to use Spring WS as a client with JAXB for the data binding and how to add pre and post processing behaviour with interceptors.

If you’re not familiar with generating JAXB classes, then take a look at this tutorial.

How to use the WebServiceTemplate class

Since Spring WS doesn’t use a service contract, you must know the request and response type. The test below demonstrates how to create and instantiate a request object of a JAXB generated class, call the marshallSendAndReceive method with it and how to cast the response object to an object of the JAXB generated response class. Finally, the test asserts that the service returned the expected result.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-config.xml")
public class AccountBalanceServiceTest {

	@Autowired
	private WebServiceTemplate webServiceTemplate;

	@Test
	public void testAccountBalanceService() {

		AccountBalanceRequest request = new ObjectFactory()
				.createAccountBalanceRequest();
		request.setName("Willy");
		request.setNumber("0987654321");
		AccountBalanceResponse response = (AccountBalanceResponse) webServiceTemplate
				.marshalSendAndReceive(request);

		assertEquals(BigDecimal.valueOf(100.5), response.getBalance());
	}
}

As you see, none web service specific information. All the web service details are hiding in the configuration.

Spring configuration

The test above requires a marshaller and WebServiceTemplate bean. The marshaller must specify the context path. That’s the package name for the generated JAXB classes.

<oxm:jaxb2-marshaller id="marshaller"
	contextPath="com.redpill.linpro.ws.account.balance.service" />

The template bean must specify the JAXB marshaller and the URI to the web service’s servlet URI.

<bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">

	<property name="marshaller" ref="marshaller" />
	<property name="unmarshaller" ref="marshaller" />
	<property name="defaultUri"
		value="http://localhost:8081/ws-demo/account-balance-service" />
</bean>

Retrieving standard SOAP faults

When the SOAP response contains a SOAP fault, then Spring WS converts it into a SoapFaultClientException. This test calls a web service and expects an exception back:

@Test(expected = SoapFaultClientException.class)
public void testAccountBalanceServiceWithTooLongAccountNumber() {

	AccountBalanceRequest request = new AccountBalanceRequest();
	request.setName("Willy");
	request.setNumber("0987654321000000000000000000000000000");

	webServiceTemplate.marshalSendAndReceive(request);
}

It’s caught by the JUnit test in the same way as you can catch every Java exceptions.

Validating

The client part of Spring WS can validate the parsed XML before it sends the XML document. You only need to specify a validator interceptor in the configuration and reference to it from the WebServiceTemplate bean.

<bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
	<property name="marshaller" ref="marshaller" />
	<property name="unmarshaller" ref="marshaller" />
	<property name="defaultUri"
		value="http://localhost:8081/ws-demo/account-balance-service" />
	<property name="interceptors">
		<list>
			<ref bean="payloadValidatingInterceptor" />
		</list>
	</property>
</bean>

<bean id="payloadValidatingInterceptor"
	class="org.springframework.ws.client.support.interceptor.PayloadValidatingInterceptor">
	<property name="schema"
		value="file:WebContent/WEB-INF/schemas/account-balance-service.xsd" />
	<property name="validateRequest" value="true" />
	<property name="validateResponse" value="true" />
</bean>

This configuration should have worked. But it doesn’t work on my machine with Spring WS 1.5.9.A. A snippet from the odd log message:

<XML validation error on request: cvc-complex-type.3.2.2: Attribute 'name' is not allowed to appear in element 'ns2:accountBalanceRequest'.>
<XML validation error on request: cvc-complex-type.4: Attribute 'name' must appear on element 'ns2:accountBalanceRequest'.>

You can click the link to see the whole log message.

ERROR [org.springframework.ws.client.support.interceptor.PayloadValidatingInterceptor] - <XML validation error on request: cvc-complex-type.3.2.2: Attribute 'name' is not allowed to appear in element 'ns2:accountBalanceRequest'.>
ERROR [org.springframework.ws.client.support.interceptor.PayloadValidatingInterceptor] - <XML validation error on request: cvc-complex-type.3.2.2: Attribute 'number' is not allowed to appear in element 'ns2:accountBalanceRequest'.>
ERROR [org.springframework.ws.client.support.interceptor.PayloadValidatingInterceptor] - <XML validation error on request: cvc-complex-type.4: Attribute 'name' must appear on element 'ns2:accountBalanceRequest'.>
ERROR [org.springframework.ws.client.support.interceptor.PayloadValidatingInterceptor] - <XML validation error on request: cvc-complex-type.4: Attribute 'number' must appear on element 'ns2:accountBalanceRequest'.>

It’s an issue with the SAX validator and the DOM source element that represents the parsed XML. A quick fix to solve this problem is to transform the DOM source to a String. Create a StringReader with the String and create a StreamSource object with the StringReader. For all the details, see the source code:

public class PayloadValidatingInterceptorWithSourceFix extends
		PayloadValidatingInterceptor {

	@Override
	protected Source getValidationRequestSource(WebServiceMessage request) {
		return transformSourceToStreamSourceWithStringReader(request.getPayloadSource());
	}

	@Override
	protected Source getValidationResponseSource(WebServiceMessage response) {
		return transformSourceToStreamSourceWithStringReader(response.getPayloadSource());
	}

	Source transformSourceToStreamSourceWithStringReader(Source notValidatableSource) {
		final Source source;
		try {
			Transformer transformer = TransformerFactory.newInstance().newTransformer();

			transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,
					"yes");
			transformer.setOutputProperty(OutputKeys.INDENT, "no");
			StringWriter writer = new StringWriter();
			transformer.transform(notValidatableSource, new StreamResult(
					writer));

			String transformed = writer.toString();
			StringReader reader = new StringReader(transformed);
			source = new StreamSource(reader);

		} catch (TransformerException transformerException) {
			throw new WebServiceTransformerException(
					"Could not convert the source to a StreamSource with a StringReader",
					transformerException);
		}

		return source;
	}
}

And finally, replace the Spring interceptor bean with this bean:

<bean id="payloadValidatingInterceptorWithSourceFix"
	class="com.redpill.linpro.transport.account.ws.PayloadValidatingInterceptorWithSourceFix">
	<property name="schema"
		value="file:WebContent/WEB-INF/schemas/account-balance-service.xsd" />
	<property name="validateRequest" value="true" />
	<property name="validateResponse" value="true" />
</bean>

When the PayloadValidatingInterceptor work as expected, the interceptor will throw a WebServiceValidationException when you try to send a value that’s not valid according to the XSD schema.

Summary

It’s an elegant framework that’s easy to use.

The validation issues is a minus, but since it’s an open-source project, you always have the chance to replace or extend the classes you want.

The code used in this article was developed in my work time at Redpill Linpro. It can be downloaded here


Redpill Linpro is the leading provider of Professional Open Source services and products in the Nordic region.

Written by Espen

February 28, 2010 at 21:50

Follow

Get every new post delivered to your Inbox.

Join 37 other followers