Search This Blog

Monday 7 October 2013

SOAP Action Header

How does a simple WSDL file look?
There will be a portType element that is like an interface definition of the web-service. It would tell us :
  1. What type of input is expected and what output would be returned.
  2. Any headers they would be a part of the input/output messages.
  3. Faults generated if any. 
 There would be a binding element associated with the port type. Its like an implementation of the port type. It would give message format and protocol details:
  1. That the protocol used is SOAP.
  2. The message style - Document/RPC.
  3. The transport mechanism - HTTP/ SMTP. 
  4. In case of SOAP, a SOAP action to help identify each of the operations.
The final component is the service element. It tells the web service clients
  1. where to access the service,
  2. via which port
  3. how the messages are defined.
The above information is represented in a port element. Each port elements is associated with a binding.
So why would a service have multiple port elements ? Why would you provide two ways to use the same operation to your clients?
The question is equivalent to "Why would an interface have two implementations ?" The one word answer to the two question would be - alternatives.
Maybe you have two versions of your web-service - Providing two ports in the service - say a OPERATION_NEW_PORT and OPERATION_OLD_PORT will allow the client to decide which of two implementations suit his need.
Maybe we need to provide transport layer variations - an HTTP version as well as an SMTP version. So an email client as well as an HTTP client could use the service.
Maybe you have varied implementations - a bare bones implementation for internal users and a more complex system (involving auditing, header checks etc) for external world users.
Or I just want to provide multiple endpoints (who is gonna stop me !!)
The point is there is a very good chance that the web-service will have to support multiple ports. I decided to add a new port to the random service.
I first defined a new binding (or a new implementation of the portType):
<wsdl:binding name="SampleServiceOperationsPortTypeSoap_V2"
      type="tns:SampleServiceOperationsPortType">
      <soap:binding style="document"
         transport="http://schemas.xmlsoap.org/soap/http" />

      <wsdl:operation name="random">
         <soap:operation soapAction="/Service/random/V2" />
         <wsdl:input name="GetRandomRequest">
            <soap:body use="literal" />
         </wsdl:input>

         <wsdl:output name="GetRandomResponse">
            <soap:body use="literal" />
         </wsdl:output>
      </wsdl:operation>
   </wsdl:binding>
This binding is same as the existing binding (except for the name of course). The next step would be to update the service element:
<wsdl:service name="SampleServiceOperationsPortTypeSoap">
      <wsdl:port binding="tns:SampleServiceOperationsPortTypeSoap"
         name="SampleServiceOperationsPortTypeSoap">
         <soap:address location="randomService" />
      </wsdl:port>

      <wsdl:port binding="tns:SampleServiceOperationsPortTypeSoap_V2"
         name="SampleServiceOperationsPortTypeSoap_V2">
         <soap:address location="randomServiceV2" />
      </wsdl:port>
   </wsdl:service>
Here I now have two port elements - each associated with a different binding. I could also have associated the two with the same binding.
The most important part is the address element of the soap namespace. One port is exposed at "randomService" and the other is exposed at "randomServiceV2".
I decided to execute a wsdl2java (CXF) on the wsdl file. The dummy server class I received was for one of the ports only - the first one. So I decided to complete the implementations.
First the interface:
@WebService(targetNamespace = "http://ws.com/Service/samplews-ns", name = "SampleServiceOperationsPortType")
@XmlSeeAlso({ ObjectFactory.class })
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface SampleServiceOperationsPortType{
  /**
   * Returns a random value from the application
   * 
   */

  @WebResult(name = "GetRandomResponse", 
      targetNamespace = "http://ws.com/Service/xsd/random-schema", partName = "GetRandomResponse")
  public GetRandomResponse random(
      @WebParam(partName = "GetRandomRequest", 
          name = "GetRandomRequest", targetNamespace = "http://ws.com/Service/xsd/random-schema") 
          GetRandomRequest getRandomRequest);

}
As seen the interface has exactly one operation - getRandom which is exposed by two endpoints. The old version is:
@javax.jws.WebService(serviceName = "SampleServiceOperationsPortTypeSoap", 
    portName = "SampleServiceOperationsPortTypeSoap", targetNamespace = "http://ws.com/Service/samplews-ns", 
    wsdlLocation = "random.wsdl", 
    endpointInterface = "com.ws.service.samplews_ns.SampleServiceOperationsPortType")
public class SampleServiceOperationsPortTypeImpl implements SampleServiceOperationsPortType {

  private static final Logger LOG = Logger.getLogger(SampleServiceOperationsPortTypeImpl.class.getName());

  @Override
  @WebMethod(action = "/Service/random")
  public GetRandomResponse random(final GetRandomRequest getRandomRequest) {
    LOG.info("Executing operation random - OLD VERSION !!!");
    try {
      final GetRandomResponse _return = new GetRandomResponse();
      _return.setValue((int) (Math.random() * 165));
      return _return;
    } catch (final java.lang.Exception ex) {
      ex.printStackTrace();
      throw new RuntimeException(ex);
    }
  }
}
I manually added the second endpoint:
@javax.jws.WebService(serviceName = "SampleServiceOperationsPortTypeSoap", 
portName = "SampleServiceOperationsPortTypeSoap", 
targetNamespace = "http://ws.com/Service/samplews-ns", wsdlLocation = "random.wsdl", 
endpointInterface = "com.ws.service.samplews_ns.SampleServiceOperationsPortType")
public class SampleServiceOperationsPortTypeImplV2 implements SampleServiceOperationsPortType {

  private static final Logger LOG = Logger.getLogger(SampleServiceOperationsPortTypeImplV2.class.getName());

  @Override
  @WebMethod(action = "/Service/random/V2")
  public GetRandomResponse random(final GetRandomRequest getRandomRequest) {
    LOG.info("Executing operation random - NEW VERSION !!!");
    try {
      final GetRandomResponse _return = new GetRandomResponse();
      _return.setValue(-101);
      return _return;
    } catch (final java.lang.Exception ex) {
      ex.printStackTrace();
      throw new RuntimeException(ex);
    }
  }
}
As seen above the two classes implement the same webservice. The two endpoints have different soap action headers. Hence the WebMethod annotation has been placed not in the interface but in the implementation class.
If we were to test the two operations by posting our requests to the two URLs the different web services would be called.
An interesting thought that came to mind is, since the two endpoints differ in SOAP Action can we get them to have the same URL ? Would CXF use a combination of URL and SOAP Action header to uniquely identify the endpoint ?
The expectation would be that when a request is received at the operation address, CXF would check the SOAPAction header to decide where the URLs must be redirected to.
Accordingly I modified my endpoint beans to use the same URL:
<jaxws:endpoint id="randomWs_V1"
      implementor="com.ws.service.samplews_ns.SampleServiceOperationsPortTypeImpl"
      endpointName="SampleServiceOperationsPortTypeSoap" address="randomService" />
<jaxws:endpoint id="randomWs_V2"
      implementor="com.ws.service.samplews_ns.SampleServiceOperationsPortTypeImplV2"
      endpointName="SampleServiceOperationsPortTypeSoap_V2" address="randomService" />
The elements differ in their values for the endpointName attribute. This maps to the value of the port in the wsdl service element.
While the theory seemed fine (to me), the application failed at start up:
8135 [localhost-startStop-1] ERROR org.springframework.web.context.ContextLoader 
- Context initialization failed
org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'randomWs_V2': Invocation of init method failed;
nested exception is javax.xml.ws.WebServiceException:
java.lang.RuntimeException: Soap 1.1 endpoint already registered on 
address randomService
The Server was least interested in the Soap Action header. A few days later I had an opportunity to attend a training session on web services at our office. The trainer cleared my queries regarding SOAP Action header as:
"This is not a SOAP Header but a HTTP Header. It is therefore not a part of the SOAP but the underlying transport layer. Hence it is not strongly coupled to SOAP implementation by SOAP web services provider. Because of its separation from the SOAP envelope its use is strongly discouraged"
I guess that kind of makes sense.

No comments:

Post a Comment