Spring WS - SOAPAction Header Example

According to the SOAP 1.1 specification, the SOAPAction HTTP header field can be used to indicate the intent of a request. There are no restrictions on the format and a client MUST use this header field when sending a SOAP HTTP request.

The below example illustrates how a client can set the SOAPAction header and how a server endpoint can leverage the @SoapAction annotation to receive the request using Spring-WS, Spring Boot, and Maven.

Client SoapActionCallback Setup

Spring WS by default sends an empty SOAPAction header. In order to set the value, we need to configure it on the WebServiceTemplate by passing a WebServiceMessageCallback which gives access to the message after it has been created, but before it is sent.

There is a dedicated SoapActionCallback class which already implements a WebServiceMessageCallback that sets the SOAPAction header. Just pass a new instance to the WebServiceTemplate in order to set it up.

Alternatively you can implement your own WebServiceMessageCallback class and set the SOAPAction on the SoapMessage using the setSoapAction() method.

packagecom.codenotfound.ws.client;importjava.math.BigInteger;importjava.util.List;importjavax.xml.bind.JAXBElement;importorg.example.ticketagent.ObjectFactory;importorg.example.ticketagent.TFlightsResponse;importorg.example.ticketagent.TListFlights;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.ws.client.core.WebServiceTemplate;importorg.springframework.ws.soap.client.core.SoapActionCallback;@ComponentpublicclassTicketAgentClient{@AutowiredprivateWebServiceTemplatewebServiceTemplate;@SuppressWarnings("unchecked")publicList<BigInteger>listFlights(){ObjectFactoryfactory=newObjectFactory();TListFlightstListFlights=factory.createTListFlights();JAXBElement<TListFlights>request=factory.createListFlightsRequest(tListFlights);// use SoapActionCallback to add the SOAPActionJAXBElement<TFlightsResponse>response=(JAXBElement<TFlightsResponse>)webServiceTemplate.marshalSendAndReceive(request,newSoapActionCallback("http://example.com/TicketAgent/listFlights"));returnresponse.getValue().getFlightNumber();}}

Endpoint @SoapAction Annotation

The endpoint mapping is responsible for mapping incoming messages to appropriate endpoints. The @SoapAction annotation marks methods with a particular SOAP Action. Whenever a message comes in which has this SOAPAction header, the method will be invoked.

In this example instead of the @PayloadRoot mapping, we will use @SoapAction to trigger the listFlights() method of our TicketAgentEndpoint class. The annotation takes as value the SOAPAction string.

The value of the SOAPAction header can be accessed within the endpoint logic by calling the getSoapAction() method on the request SoapMessage. Just add the MessageContext as an input parameter in order to retrieve it.

packagecom.codenotfound.ws.endpoint;importjava.math.BigInteger;importjavax.xml.bind.JAXBElement;importorg.example.ticketagent.ObjectFactory;importorg.example.ticketagent.TFlightsResponse;importorg.example.ticketagent.TListFlights;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.ws.WebServiceMessage;importorg.springframework.ws.context.MessageContext;importorg.springframework.ws.server.endpoint.annotation.Endpoint;importorg.springframework.ws.server.endpoint.annotation.RequestPayload;importorg.springframework.ws.server.endpoint.annotation.ResponsePayload;importorg.springframework.ws.soap.SoapMessage;importorg.springframework.ws.soap.server.endpoint.annotation.SoapAction;@EndpointpublicclassTicketAgentEndpoint{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(TicketAgentEndpoint.class);// map a message to this endpoint based on the SOAPAction@SoapAction(value="http://example.com/TicketAgent/listFlights")@ResponsePayloadpublicJAXBElement<TFlightsResponse>listFlights(@RequestPayloadJAXBElement<TListFlights>request,MessageContextmessageContext){// access the SOAPAction valueWebServiceMessagewebServiceMessage=messageContext.getRequest();SoapMessagesoapMessage=(SoapMessage)webServiceMessage;LOGGER.info("SOAPAction: '{}'",soapMessage.getSoapAction());ObjectFactoryfactory=newObjectFactory();TFlightsResponsetFlightsResponse=factory.createTFlightsResponse();tFlightsResponse.getFlightNumber().add(BigInteger.valueOf(101));returnfactory.createListFlightsResponse(tFlightsResponse);}}

Testing the SOAPAction Header

Now that we have setup the SOAPAction in both client and server let’s write some unit test cases to test the correct working.

For the client, we will use a MockWebServiceServer in combination with a custom SoapActionMatcher in order to verify that the SOAPAction header has been set. Based on following Stack Overflow example we implement the RequestMatcher interface and assert that an expected SOAPAction is present.

In the test case, we add the SoapActionMatcher as expected match to the MockWebServiceServer and check the result by calling the verify() method.

packagecom.codenotfound.ws.client;importstaticorg.assertj.core.api.Assertions.assertThat;importstaticorg.springframework.ws.test.client.RequestMatchers.payload;importstaticorg.springframework.ws.test.client.ResponseCreators.withPayload;importjava.math.BigInteger;importjava.util.List;importjavax.xml.transform.Source;importorg.junit.Before;importorg.junit.Test;importorg.junit.runner.RunWith;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.test.context.junit4.SpringRunner;importorg.springframework.ws.client.core.WebServiceTemplate;importorg.springframework.ws.test.client.MockWebServiceServer;importorg.springframework.xml.transform.StringSource;@RunWith(SpringRunner.class)@SpringBootTestpublicclassTicketAgentClientTest{@AutowiredprivateTicketAgentClientticketAgentClient;@AutowiredprivateWebServiceTemplatewebServiceTemplate;privateMockWebServiceServermockWebServiceServer;@BeforepublicvoidcreateServer(){mockWebServiceServer=MockWebServiceServer.createServer(webServiceTemplate);}@TestpublicvoidtestListFlights(){SourcerequestPayload=newStringSource("<ns3:listFlightsRequest xmlns:ns3=\"http://example.org/TicketAgent.xsd\">"+"</ns3:listFlightsRequest>");SourceresponsePayload=newStringSource("<v1:listFlightsResponse xmlns:v1=\"http://example.org/TicketAgent.xsd\">"+"<flightNumber>101</flightNumber>"+"</v1:listFlightsResponse>");// check if the SOAPAction is present using the custom matchermockWebServiceServer.expect(newSoapActionMatcher("http://example.com/TicketAgent/listFlights")).andExpect(payload(requestPayload)).andRespond(withPayload(responsePayload));List<BigInteger>flights=ticketAgentClient.listFlights();assertThat(flights.get(0)).isEqualTo(BigInteger.valueOf(101));mockWebServiceServer.verify();}}

The endpoint setup is tested by first creating a custom SoapActionCreator which implements the RequestCreator interface. This is done to provide a WebServiceMessage on which the SOAPAction has been set as this is not the case with the default request creators.

Simply create the message using the supplied XML Source and then set the SOAPAction using the setSoapAction() method.

The test calls the sendRequest() of the MockWebServiceClient with the custom SoapActionCreator which results in a request message for the endpoint to consume. If the correct SOAPAction is present the request is mapped to the endpoint and a response is returned.