Spring Boot SOAP Client

18 June 2016

This is how I implemented "Consuming a SOAP Web Service" from Spring Guides. For the RESTful world, SOAP is dead but it's pretty much alive and Spring is developing features for it. I'm not yet sure why the web service we are integrating to has extra requirements not available in REST but if you're wondering why companies are using SOAP, here is a good REST vs SOAP article.

To get started with SOAP, I have to add this to my existing Spring Boot POM.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-ws</artifactId>
</dependency>

The above Spring Boot starter will add the following dependencies.

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jms</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-oxm</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.ws</groupId>
	<artifactId>spring-ws-core</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.ws</groupId>
	<artifactId>spring-ws-support</artifactId>
</dependency>

Generating POJOs using WSDL

Since I'm going to build a client that fetches weather data from CDYNE weather, I need POJOs as request and response objects. To generate them, I'm going to add this to my POM's plugins.

<plugin>
	<groupId>org.jvnet.jaxb2.maven2</groupId>
	<artifactId>maven-jaxb2-plugin</artifactId>
	<version>0.12.3</version>
	<executions>
		<execution>
			<goals>
				<goal>generate</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<schemaLanguage>WSDL</schemaLanguage>
		<generatePackage>com.prototype.wsdl</generatePackage>
		<schemas>
			<schema>
				<url>http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl</url>
			</schema>
		</schemas>
	</configuration>
</plugin>

Everytime you build your project, maven-jaxb2-plugin will regenerate the POJOs or you can use mvn generate-sources

$ mvn generate-sources
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building patterns 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-jaxb2-plugin:0.12.3:generate (default) @ prototype ---
[INFO] Up-to-date check for source resources [[http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl, file:/home/drmanalo/workspace/prototype/pom.xml]] and taret resources [[file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/ArrayOfForecast.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/ArrayOfWeatherDescription.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/Forecast.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/ForecastReturn.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/GetCityForecastByZIP.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/GetCityForecastByZIPResponse.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/GetCityWeatherByZIP.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/GetCityWeatherByZIPResponse.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/GetWeatherInformation.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/GetWeatherInformationResponse.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/ObjectFactory.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/POP.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/Temp.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/WeatherDescription.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/WeatherReturn.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/com/prototype/wsdl/package-info.java, file:/home/drmanalo/workspace/prototype/target/generated-sources/xjc/META-INF/sun-jaxb.episode]].
[WARNING] The URI [http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl] seems to represent an absolute HTTP or HTTPS URL. Getting the last modification timestamp is only possible if the URL is accessible and if the server returns the [Last-Modified] header correctly. This method is not reliable and is likely to fail. In this case the last modification timestamp will be assumed to be unknown.
[ERROR] Could not retrieve the last modification timestamp for the URI [http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl] from the HTTP URL connection. The [Last-Modified] header was probably not set correctly.
[WARNING] Last modification of the URI [http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl] is not known.
[INFO] Sources are not up-to-date, XJC will be executed.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.138 s
[INFO] Finished at: 2016-06-19T09:01:55+01:00
[INFO] Final Memory: 13M/161M
[INFO] ------------------------------------------------------------------------

Weather client

package com.prototype.weather;

import java.text.SimpleDateFormat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ws.client.core.support.WebServiceGatewaySupport;
import org.springframework.ws.soap.client.core.SoapActionCallback;

import com.prototype.wsdl.Forecast;
import com.prototype.wsdl.ForecastReturn;
import com.prototype.wsdl.GetCityForecastByZIP;
import com.prototype.wsdl.GetCityForecastByZIPResponse;
import com.prototype.wsdl.Temp;

public class WeatherClient extends WebServiceGatewaySupport {

	private static final Logger LOGGER = LoggerFactory.getLogger(WeatherClient.class);

	public GetCityForecastByZIPResponse getCityForecastByZip(String zipCode) {

		GetCityForecastByZIP request = new GetCityForecastByZIP();
		request.setZIP(zipCode);

		LOGGER.info("Requesting forecast for " + zipCode + "...");

		GetCityForecastByZIPResponse response = (GetCityForecastByZIPResponse) getWebServiceTemplate()
				.marshalSendAndReceive("http://wsf.cdyne.com/WeatherWS/Weather.asmx", request,
						new SoapActionCallback("http://ws.cdyne.com/WeatherWS/GetCityForecastByZIP"));

		return response;
	}

	public void printResponse(GetCityForecastByZIPResponse response) {

		ForecastReturn forecastReturn = response.getGetCityForecastByZIPResult();

		if (forecastReturn.isSuccess()) {

			LOGGER.info("Forecast for " + forecastReturn.getCity() + ", " + forecastReturn.getState());

			SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
			for (Forecast forecast : forecastReturn.getForecastResult().getForecast()) {

				Temp temperature = forecast.getTemperatures();

				LOGGER.info(String.format("%s %s %s°-%s°",
						format.format(forecast.getDate().toGregorianCalendar().getTime()), forecast.getDesciption(),
						temperature.getMorningLow(), temperature.getDaytimeHigh()));
			}
		} else {
			LOGGER.info("No forecast received");
		}
	}

}

Weather Client configuration

package com.prototype;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

import com.prototype.weather.WeatherClient;

@Configuration
public class WeatherClientConfiguration {

	@Bean
	public Jaxb2Marshaller marshaller() {
		final Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
		marshaller.setContextPath("com.prototype.wsdl");
		return marshaller;
	}

	@Bean
	public WeatherClient weatherClient(Jaxb2Marshaller marshaller) {
		final WeatherClient client = new WeatherClient();
		client.setDefaultUri("http://ws.cdyne.com/WeatherWS");
		client.setMarshaller(marshaller);
		client.setUnmarshaller(marshaller);
		return client;
	}
}

Weather Client Test

package com.prototype.weather;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import com.prototype.PrototypeApplication;
import com.prototype.WeatherClientConfiguration;
import com.prototype.weather.WeatherClient;
import com.prototype.wsdl.GetCityForecastByZIPResponse;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = PrototypeApplication.class)
@WebAppConfiguration
public class WeatherClientTest {

	@Test
	public void testWeatherClient() {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WeatherClientConfiguration.class);
		WeatherClient weatherClient = context.getBean(WeatherClient.class);
		GetCityForecastByZIPResponse response = weatherClient.getCityForecastByZip("94304");
		weatherClient.printResponse(response);
		context.close();
	}
}

You should see this on your console

09:06:08.626  INFO [main] 57: StartupInfoLogger.logStarted -- Started WeatherClientTest in 3.314 seconds (JVM running for 4.265)
09:06:08.647  INFO [main] 578: AbstractApplicationContext.prepareRefresh -- Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@7c84195: startup date [Sun Jun 19 09:06:08 BST 2016]; root of context hierarchy
09:06:08.661  INFO [main] 494: Jaxb2Marshaller.createJaxbContextFromContextPath -- Creating JAXBContext with context path [com.prototype.wsdl]
09:06:08.697  INFO [main] 139: SaajSoapMessageFactory.afterPropertiesSet -- Creating SAAJ 1.3 MessageFactory with SOAP 1.1 Protocol
09:06:08.704  INFO [main] 25: WeatherClient.getCityForecastByZip -- Requesting forecast for 94304...
09:06:09.889  INFO [main] 40: WeatherClient.printResponse -- Forecast for Palo Alto, CA
09:06:09.891  INFO [main] 47: WeatherClient.printResponse -- 2013-01-03 Partly Cloudy °-57°
09:06:09.891  INFO [main] 47: WeatherClient.printResponse -- 2013-01-04 Partly Cloudy 41°-58°
09:06:09.892  INFO [main] 47: WeatherClient.printResponse -- 2013-01-05 Partly Cloudy 41°-59°
09:06:09.892  INFO [main] 47: WeatherClient.printResponse -- 2013-01-06 Partly Cloudy 44°-56°
09:06:09.892  INFO [main] 47: WeatherClient.printResponse -- 2013-01-07 Partly Cloudy 41°-60°
09:06:09.893  INFO [main] 47: WeatherClient.printResponse -- 2013-01-08 Partly Cloudy 42°-60°
09:06:09.893  INFO [main] 47: WeatherClient.printResponse -- 2013-01-09 Partly Cloudy 43°-58°