When “Generate from WSDL” fails – hand-coding web service calls

The starting point for invoking a web service from Force.com is to see what happens when you use the “Generate From WSDL” button on the WSDL. See e.g. Apex Web Services and Callouts. This mechanism is also sometimes referred to as WSDL2Apex.

Most WSDLs will need various edits to comply with the limitations of the mechanism and it is worth trying hard to make the necessary changes. If you succeed then you will have generated Apex code that is in theory compliant with the WSDL and its embedded schemas. Most importantly you will have classes representing the request and response objects that make creating requests and processing responses simple. You will have to do some hacking of the generated classes to provide entry points for tests to achieve reasonable code coverage. Also I had one case where the code generated but was broken at run-time because of a rather unusual no-namespace element so test the generated code before declaring victory. (The API that the generated code works against is not documented which is a pity.) And as you can’t invoke a web service from an Apex test you will have to put together some UI to do the testing.

But unless the WSDL has been designed with Force.com in mind you may find that this approach is a dead end. Facilities for hand-coding in these cases have improved and this article provides an example of this. If all you need to do is make a few simple requests and process a few simple responses then this is a viable if undesirable option. But if you are working against a service that changes constantly or has a large number of requests and responses or complex requests and responses then you are in trouble…

My #1 piece of advice is to try out the service first using more capable tools. I used Axis hosted in Eclipse, with wsdl2java succeeding in generating the service client code where Force.com had failed. That then made it quick to write a JUnit test to exercise the service. And most importantly Axis includes a proxy server tcpmon that is easy to setup and lets you capture the request and response XML and also the HTTP headers. So you can establish exactly what works and use this as a reference when your Apex implementation is silently failing. (I couldn’t see a way to examine the request and response XML and headers in Force.com, and routing through something like tcpmon would require tcpmon to be running on a machine that is accessible from the internet.)

Note that I had some pain when working in Axis in that the service I was invoking was created using .Net and had very specific needs in how the addressing SOAP headers were handled. Google and the open nature of Axis came to the rescue.

So with an understanding of what is needed you can go ahead and write the code. Here is what I ended up with. This code is using DOM classes but consider using XmlStreamWriter and XmlStreamReader if they fit your data patterns better. Note this code won’t run – I’ve removed specifics like the endpoint and actual namespace.

First a base class to reduce code duplication:

public abstract class SampleBase {

    public static final String SE_NAMESPACE = 'http://www.w3.org/2003/05/soap-envelope';
    public static final String SE_PREFIX = 'se';

    public static final String TE_NAMESPACE = 'http://tempuri.org/';
    public static final String TE_PREFIX = 'te';

    public static final String MD_NAMESPACE = 'http://sample.com';
    public static final String MD_PREFIX = 'md';

    public static final String AD_NAMESPACE = 'http://www.w3.org/2005/08/addressing';
    public static final String AD_PREFIX = 'ad';

    public class SampleException extends Exception {
    }

    /**
     * Overall wrapping method. Call this to invoke the particular operation.
     */
    public virtual Object invoke() {

        HttpRequest request = createRequest();

        // Avoid unsupported media type error
        request.setHeader('Content-Type', 'application/soap+xml; charset=UTF-8');

        System.debug('request=' + request.getBody());

        Http h = new Http();
        HttpResponse response = h.send(request);
        if (response.getStatusCode() != 200) {
            throw new SampleException('Not ok;'
                    + ' statusCode='+ response.getStatusCode()
                    + ' status=' + response.getStatus()
                    );
        }
        System.debug('response=' + response.getBody());
        return parseResponse(response);
    }

    public virtual HttpRequest createRequest() {
        HttpRequest request = new HttpRequest();
        request.setEndPoint(getUrl());
        request.setMethod('POST');
        request.setBody(createRequestDocument().toXmlString());
        return request;
    }

    public abstract Dom.Document createRequestDocument();

    public virtual Object parseResponse(HttpResponse response) {
        return parseResponseDocument(response.getBodyDocument());
    }

    public abstract Object parseResponseDocument(Dom.Document response);

    protected String getUrl() {
        return 'http://sample.com/WebService/SampleService';
    }

    protected String getLicenseKey() {
        return 'a license key from e.g. custom settings';
    }
}

Then here is an example of one operation. This is the simplest of the ones I needed to do, just returning a string session id that then needed to be passed into other operations:

public class SampleAuthenticate extends SampleBase {

    private static final String ACTION_URL = 'http://tempuri.org/SampleService/Authenticate';

    public override HttpRequest createRequest() {
        HttpRequest request = super.createRequest();
        request.setHeader('SOAPAction', ACTION_URL);
        return request;
    }

    public override Dom.Document createRequestDocument() {

        Dom.Document doc = new Dom.Document();
        Dom.XMLNode envelope = doc.createRootElement('Envelope', SE_NAMESPACE, SE_PREFIX);

        Dom.XMLNode header = envelope.addChildElement('Header', SE_NAMESPACE, SE_PREFIX);
        header.addChildElement('To', AD_NAMESPACE, AD_PREFIX).addTextNode(getUrl());
        header.addChildElement('Action', AD_NAMESPACE, AD_PREFIX).addTextNode(ACTION_URL);

        envelope.addChildElement('Body', SE_NAMESPACE, SE_PREFIX)
                .addChildElement('Authenticate', TE_NAMESPACE, TE_PREFIX)
                .addChildElement('licenseKey', TE_NAMESPACE, TE_PREFIX)
                .addTextNode(getLicenseKey());

        return doc;
    }

    public override Object parseResponseDocument(Dom.Document response) {

        Object sessionId = response
                .getRootElement()
                .getChildElement('Body', SE_NAMESPACE)
                .getChildElement('AuthenticateResponse', TE_NAMESPACE)
                .getChildElement('AuthenticateResult', TE_NAMESPACE)
                .getChildElement('SessionId', MD_NAMESPACE)
                .getText();
        return sessionId;
    }
}

And here is a test case that covers the request creation (to a degree) and checks that a sample response XML file (obtained using tcpmon) and stored as a static resource can be parsed and produces the right result:

@isTest
private class SampleAuthenticateTest {

    @isTest
    static void testCreateRequest() {
        String xml = new SampleAuthenticate().createRequestDocument().toXmlString();
        System.debug('xml=' + xml);
        System.assertNotEquals(null, xml);
        System.assert(xml.length() > 100);

        // Not much value in more asserts here; its what the remote service thinks that matters
    }

    @isTest
    static void testParseResponse() {
        Object sessionId = new SampleAuthenticate().parseResponseDocument(
                getDocument('TestSampleAuthenticationResponse')
                );
        System.assertEquals('session id in sample response xml', sessionId);
    }

    private static Dom.Document getDocument(String staticResourceName) {
        StaticResource sr = [select Body from StaticResource where Name = :staticResourceName];
        Dom.Document doc = new Dom.Document();
        doc.load(sr.Body.toString());
        return doc;
    }
}

In case you are thinking that this doesn’t look too bad, keep in mind that most service operations have multiple request arguments and return complex objects with repeating elements and multiple levels of nesting. And the XML Schema referenced by the WSDL can model a huge variety of structures – any decent XML Schema reference is hundreds of pages long – so the variations that your code can have to deal with if the service implementor has got fancy or clever are frightening.

Even in simple cases that leaves you manually creating multiple data holding classes. You should probably make each data holder responsible for its own parsing by passing the relevant Dom.XMLNode to it. You should be able to Google various patterns for this type of code from the bad old days before tooling like Java’s JAXB made this sort of work unnecessary in other technology stacks. And after a day of coding these data holders and a pile of test cases to make sure they work, and then another day of NPEs when you run against the real service, just remember that JAXB would do that same work in seconds…

One final oddity is that the invoke method in the base class returning an Object seemed like an obvious way to go so the caller can downcast as needed. But when you implement an operation that returns a List of objects you discover that in Apex a List is not an Object at all. Yuk.

About these ads

2 thoughts on “When “Generate from WSDL” fails – hand-coding web service calls

  1. I think that Force.com has to improve this code generator as this is a common issue for Force.com developers. If someone has time maybe writing and selling a new apex code generator (as plugin for eclipse ), then he will get rich quickly …

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s