How to expose a public Salesforce Web Service using a top-down approach (contract first) WSDL interface ?

Problem

How can we expose a public Web service using Salesforce (e.g no authentication needed to access this web service) ? Let’s add some challenge by using a WSDL contract imposed by your service Provider and that you have to follow when implementing this web service using Salesforce.

Background

I have proposed a solution to support multiple delegated authentication service when having one Salesforce Organisation and said that this service can be exposed in Salesforce. After some investigation, it seems a little bit challenging to use a top-down approach when implementing web service in Salesforce. Why ?
There is no (documented?) way to expose a Web Service in Salesforce starting from WSDL. If you are coming from Java World, there is an option -server when you used WSDL2Java to generate the skeleton classes for the Web service implementation. I do not find a equivalent option in Salesforce using WSDL2Apex. My understanding of WSDL2Apex is that it’s provides you the stub classes to call a Web Service from Salesforce (Web Service Callout).
Salesforce propose a bottom-up model to implement your Web Service. The drawback of this approach is that you cannot set the different namespace/target namespace inherent to your Web Service, as it is automatically generated by Salesforce and you cannot override it (yet ?).

Proposed Solution

I have been working with Web Service since decade now and I remember that we can expose/call Web Service using the old plain HTTPRequest/ HTTPResponse with XML over HTTP(/HTTPS). This seems to be the right path to go. But in order to not re-inventing the wheel, i will “deviate” the REST annotation from Salesforce, and adapt it to my need to serve SOAP Request/Response (@RestResource annotation) imposed by the WSDL contract. You will find this strange and a bit overwhelmed solution (and yes it is ;-)), but I do not find any other solution. I really hope that Salesforce will proposed a way to expose a Web Service starting from the WSDL contract.

Required Steps

  • Get the WSDL Contract, in our case I will use the DelegatedAuthentication.wsdl exposed by Salesforce
  • Use the excellent SOAPUI tools to create SOAP requests and SOAP responses covering all uses cases. Do not forget to handle properly the SOAP Exception that might happened
  • To store theses SOAP templates, I was first thinking using Salesforce Custom Settings, but I found that I cannot create a text document greater that 255 characters. So converge to use the Static Resources for any Web Site published by Salesforce. Then I just have to load the SOAP Request/Response from this static resource and do required processing on thoses files (replacing tags template).
  • Create REST Apex Class that support @HttpGet and @HttpPost annotation. @HttpGet will just return the WSDL exposed by the REST Service and @Httppost will handle the SOAP request.
  • Go to you Org | Setup | Remote Site and add your remote site URL to avoid the error “Unauthorized endpoint”
  • Zip your static resources (SOAP Request and Response + WSDL Interface contract). Go to you Org | Setup | Static Resource and add your zip file. This access the XML file from the static resource, just use the following URL: https://cmok-developer-edition.eu5.force.com/developper/resource/SR_DelegatedAuthenticationService/DAS_Response_true.xml where
      developper is your site name,
      SR_DelegatedAuthenticationService is the name of your static resource,
      and DAS_Response_true.xml is the name of your XML file in the static resource.
  • The Apex code just use DOM Document to parse and validate the SOAP Request
  • Sample Apex Code

    @RestResource(urlMapping='/MultipleDelegatedAutNService')
    global class MultipleDelegatedAutNService {
        //https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_restcontext.htm
        //https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_restrequest.htm#apex_methods_system_restrequest
        @HttpGet
        global static void doGet() {
        	RestContext.response.addHeader('Content-Type', 'application/xml');
            String wsdlFile = 'https://cmok-developer-edition.eu5.force.com/developper/resource/SR_DelegatedAuthenticationService/DelegatedAuthentication.wsdl';
    		RestContext.response.responseBody = Blob.valueOf(getBody(wsdlFile));
        }
    
    
        // Just checking that it's actually XML
        private static String parseXMLStr(String toParse) {
          DOM.Document doc = new DOM.Document();
          
          try {
            doc.load(toParse);    
            DOM.XMLNode root = doc.getRootElement();
            return walkThrough(root);
            
          } catch (System.XMLException e) {  // invalid XML
            return e.getMessage();
          }
        }
    
        // Recursively walk through the XML
        /*
    		14:22:49.043 (43847766)|USER_DEBUG|[96]|DEBUG|*** parseXMLStr=
    		Element: Envelope namespace: http://schemas.xmlsoap.org/soap/envelope/
    		Element: Body namespace: http://schemas.xmlsoap.org/soap/envelope/
    		Element: Authenticate namespace: urn:authentication.soap.sforce.com
    		Element: username namespace: urn:authentication.soap.sforce.com, text=OKM.Customer.invensys@bridge-fo.com.devoct
    		Element: password namespace: urn:authentication.soap.sforce.com, text=5F91558D-0274-4423-B397-AA7065EA5074
    		Element: sourceIp namespace: urn:authentication.soap.sforce.com, text=INVENSYS
        */
        // Reference: http://developer.force.com/cookbook/recipe/parsing-xml-using-the-apex-dom-parser
        private static String walkThrough(DOM.XMLNode node) {
          String result = '\n';
          if (node.getNodeType() == DOM.XMLNodeType.COMMENT) {
            return 'Comment (' +  node.getText() + ')';
          }
          if (node.getNodeType() == DOM.XMLNodeType.TEXT) {
            return 'Text (' + node.getText() + ')';
          }
          if (node.getNodeType() == DOM.XMLNodeType.ELEMENT) {
            result += 'Element: ' + node.getName() + ' Namespace: ' + node.getNamespace();
            if (node.getText().trim() != '') {
              result += ', text=' + node.getText().trim();
            }
            if (node.getAttributeCount() > 0) { 
              for (Integer i = 0; i< node.getAttributeCount(); i++ ) {
                result += ', attribute #' + i + ':' + node.getAttributeKeyAt(i) + '=' + node.getAttributeValue(node.getAttributeKeyAt(i), node.getAttributeKeyNsAt(i));
              }  
            }
            for (Dom.XMLNode child: node.getChildElements()) {
              result += walkThrough(child);
            }
            return result;
          }
          return '';  //should never reach here
          
        }    
    
    
    	private Boolean isValid_DA_SOAPRequest(Dom.XMLNode node) {
    		Boolean isValidSoapRequest = false;
    		/*
    		left as an exercise - Applied your logic here
    		*/	        
    		return isValidSoapRequest;
    	}    
    
        private static String getBody(String url){
        	Http h = new Http();
    		HttpRequest webReq = new HttpRequest();
    		webReq.setMethod('GET');
    		webReq.setEndpoint(url);
    		HttpResponse res = h.send(webReq);
    		res.setHeader('Content-Type', 'application/xml'); 
    		return res.getbody();
        }   
    
    
        //1. Before you can access external servers from an endpoint or redirect endpoint using Apex or any other feature, you must add the remote site to a list of authorized remote sites in the Salesforce user interface. 
        //   Unauthorized endpoint, please check Setup->Security->Remote site settings - Add your site endpoint https://cmok-developer-edition.eu5.force.com
        //2.The return type of you WS MUST be void, so you can return any content type, in our case a valid SOAP response
        @HttpPost
        global static void doPost() {
        	//RestRequest restRequest = RestContext.request;
        	//String soapRequestBody= restRequest.requestBody ; 
        	String soapRequestBody = RestContext.request.requestBody.toString();
        	System.debug('*** soapRequestBody=' + soapRequestBody);
        	System.debug('*** parseXMLStr=' + parseXMLStr(soapRequestBody));    	
        	RestContext.response.addHeader('Content-Type', 'application/xml');
            String urlSuccess = 'https://cmok-developer-edition.eu5.force.com/developper/resource/SR_DelegatedAuthenticationService/DAS_Response_true.xml';
            //String urlFailed = 'https://cmok-developer-edition.eu5.force.com/developper/resource/SR_DelegatedAuthenticationService/DAS_Response_false.xml';
    		/*
    		left as an exercise - Applied your logic here
    		*/	        
    		RestContext.response.responseBody = Blob.valueOf(getBody(urlSuccess));
        }
        
    }
    

    Screen Shot of my PoC

    HTTP GET on URL https://cmok-developer-edition.eu5.force.com/developper/services/apexrest/MultipleDelegatedAutNService provide your the WSDL interface
    01-WSL Exposition from GET - 2015-09-20_17-48-59

    Calling you webservice from SOAPUI
    02-3-SOAPUI

    Configure the remote site setting to expose publically your Web Service
    02-1- Remote Site - 2015-09-20_17-31-00

    Use the static resource to store your SOAP Request/Response/ and WSDL files.
    02-2-Static resource - 2015-09-20_17-50-27

    Thanks for reading.