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.

    Advertisements

    About Chenda Mok

    19 years of hands on experience in software design and development with emphasis on Enterprise Application Integration (EAI), Services Oriented Architecture (SOA) and Identity Management (IDM) solutions. I’m a software engineer, member of the professional service delivery team working for Salesforce. Prior to this, I worked for Oracle as Solution Architect, through SeeBeyond(06/2005), then SUN’s acquisition (04/2009). After my master’s degree in computer science in 1997; I always delivered consulting on architecture, design, implementation on integration’s field. I’m interested in architecture using EAI/SOA/IDM/BPM/Cloud technologies, software development and Java’s related technologies. I may blog about my work/activities at Salesforce, but I do not speak for my employer, past, present or future.
    This entry was posted in Salesforce and tagged , , , , , , , , , , , , , . Bookmark the permalink.