Wednesday, October 19, 2011

SOAP-Fault for a Contract First service using Spring-WS

This is a follow up to the simple webservice described in this post

Just to recap, my webservice was responsible for returning the details of a "member" given the identifier for the member. If a member was not found for the identifier, the service did not return a member.

For this request, which matches a member in the system:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mem="http://bk.org/memberservice/">
   <soapenv:Header/>
   <soapenv:Body>
      <mem:MemberDetailsRequest xmlns:mem="http://bk.org/memberservice/">
         <mem:id>1</mem:id>
      </mem:MemberDetailsRequest>
   </soapenv:Body>
</soapenv:Envelope>

this is the response:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns2:MemberDetailsResponse xmlns:ns2="http://bk.org/memberservice/">
         <ns2:memberdetail>
            <ns2:id>1</ns2:id>
            <ns2:name>john doe</ns2:name>
            <ns2:phone>111-111-1111</ns2:phone>
            <ns2:city>City</ns2:city>
            <ns2:state>State</ns2:state>
         </ns2:memberdetail>
      </ns2:MemberDetailsResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>


and for a case where there is no match, this is the response:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns2:MemberDetailsResponse xmlns:ns2="http://bk.org/memberservice/"/>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Now, what I want to do is, instead of giving a response of this form, I want my contract to explicitly declare a Fault, in case a member is not found.

There are a couple of different ways of doing it.

The first way is to simply throw a RuntimeException, this would be trapped by the Spring-WS stack(using a SimpleFaultMessageResolver) and returned as a Soap fault.
So now my endpoint implementation is:
...
@Endpoint
public class GetMemberDetailsEndpoint {
 @Autowired private MemberManager memberManager;

 @PayloadRoot(namespace = "http://bk.org/memberservice/", localPart = "MemberDetailsRequest")
 @ResponsePayload
 public MemberDetailsResponse getMemberDetails(@RequestPayload MemberDetailsRequest request) throws Exception {
  MemberDetail memberDetail = memberManager.findByMemberId(request.getId());
  if (memberDetail==null){
   throw new RuntimeException("Member Not Found");
  }
  MemberDetailsResponse response = new MemberDetailsResponse(memberDetail);
  return response;

 }
...

and the response for a case where a member is not found is a clean Soap fault with the message set in the exception as the fault detail:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <SOAP-ENV:Fault>
         <faultcode>SOAP-ENV:Server</faultcode>
         <faultstring xml:lang="en">Member Not Found</faultstring>
      </SOAP-ENV:Fault>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>


The second way is to define a custom exception class, and throw this custom exception from the endpoint:
@PayloadRoot(namespace = "http://bk.org/memberservice/", localPart = "MemberDetailsRequest")
 @ResponsePayload
 public MemberDetailsResponse getMemberDetails(@RequestPayload MemberDetailsRequest request) throws Exception {
  MemberDetail memberDetail = memberManager.findByMemberId(request.getId());
  if (memberDetail==null){
   throw new MemberDetailsFault("Member Not Found");
  }
  MemberDetailsResponse response = new MemberDetailsResponse(memberDetail);
  return response;

 }

and define a resolver, which will map exceptions of this new type(MemberDetailsFault) to a more descriptive text:
<bean class="org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver">
        <property name="defaultFault" value="SERVER"/>
        <property name="exceptionMappings">
            <value>
                org.bk.memberservice.message.MemberDetailsFault=SERVER,No Member-message from application context
            </value>
        </property>
    </bean> 

the exceptionresolver is discovered by type, so there is no need to give the bean a name. With this in place, the Soap fault, will have the fault detail based on the exceptionMapping:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <SOAP-ENV:Fault>
         <faultcode>SOAP-ENV:Server</faultcode>
         <faultstring xml:lang="en">No Member-message from application context</faultstring>
      </SOAP-ENV:Fault>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

A third way, is to annotate the custom exception class with a @SoapFault annotation, describing the exception to be returned as part of the faultdetail:
@SoapFault(faultCode = FaultCode.SERVER, faultStringOrReason = "No Member Found - From @SoapFault annotation")
public class MemberDetailsFault extends RuntimeException{


A new resolver is required to interpret exception with annotations to a Soap Fault, SoapFaultAnnotationExceptionResolver:
<bean class="org.springframework.ws.soap.server.endpoint.SoapFaultAnnotationExceptionResolver"/>

With this in place, the response is:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <SOAP-ENV:Fault>
         <faultcode>SOAP-ENV:Server</faultcode>
         <faultstring xml:lang="en">No Member Found - From @SoapFault annotation</faultstring>
      </SOAP-ENV:Fault>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

I personally prefer the third approach, it is a little more concise.

Sample available at this location: git://github.com/bijukunjummen/memberservice-contractfirst.git

No comments:

Post a Comment