Tuesday, May 15, 2018

Salesforce: Apex: Generate from WSDL

Problem

.
When trying to generate Apex from a WSDL in Salesforce, you have to do a few things to make the WSDL suitable for use with the OOTB 'Generate from WSDL' option. It is possible that the Open Source WSDL2Apex Generator (here) does not require similar preprocessing, but, as of this writing, I have not tried it, so I do not know. In this post, I am only going to focus on the two actions that I had to take to get a WSDL into a suitable form, but I am relatively certain that this is only a small subset of what one might have to do along the spectrum of WSDLs.
.

Solution

.

Step 1: Inline the Imports

.
Let's say you have a WSDL that looks something like this:
.
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="ServiceName" targetNamespace="http://[domain]/[namespace]" xmlns:tns="http://[domain]/[namespace]" [...etc...]>
    <types>
        <xsd:schema>
            <xsd:import schemaLocation="https://[domain]/[schema]" namespace="http://[domain]/[namespace]"/>
        </xsd:schema>
    </types>
    <message name="SomeMessage">
        <part name="parameters" element="tns:SomeMessage"/>
    </message>
    <portType name="ServiceName">
    <operation name="SomeMessage">
        <input message="tns:SomeMessage" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <output message="tns:SomeMessageResponse" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <fault name="ServiceException" message="tns:ServiceException" wsam:Action="http://[domain]/[namespace]/[method]/fault/ServiceException"/>
    </operation>
    <xs:complexType name="ServiceException">
        <xs:sequence>
            <xs:element name="errorCode" type="xs:int"/>
            <xs:element name="message" type="xs:string" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
    [...etc...]
</definitions>
.
The 'xsd:import' is not supported, so you have to actually inline the related schema into this WSDL, which we will refer to as the 'Consolidated_WSDL', for clarity. 
.
Let's say that related schema looks something like this:
.
<?xml version='1.0' encoding='UTF-8'?>
<xs:schema xmlns:tns="http://[domain]/[namespace_1]" version="1.0" targetNamespace="http://[domain]/[namespace_1]">
    <xs:import namespace="http://[domain]/[namespace_2" schemaLocation="https://[domain]/[namespace_2]"/>
    <xs:import namespace="http://[domain]/[namespace_3" schemaLocation="https://[domain]/[namespace_3]"/>
    <xs:element name="SomeException" type="tns:SomeException]"/>
    <xs:element name="SomeService" type="tns:SomeService"/>
    [...etc...]
</xs:schema>

.
You would inline this schema by replacing the entire <xsd:import> block in the Consolidated_WSDL with just the <schema> block from this file, as follows, in blue:
.
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="ServiceName" targetNamespace="http://[domain]/[namespace]" xmlns:tns="http://[domain]/[namespace]" [...etc...]>
    <types>
        <xs:schema xmlns:tns="http://[domain]/[namespace_1]" version="1.0" targetNamespace="http://[domain]/[namespace_1]">
            <xs:import namespace="http://[domain]/[namespace_2" schemaLocation="https://[domain]/[namespace_2]"/>
            <xs:import namespace="http://[domain]/[namespace_3" schemaLocation="https://[domain]/[namespace_3]"/>
            <xs:element name="SomeException" type="tns:SomeException]"/>
            <xs:element name="SomeService" type="tns:SomeService"/>
            [...etc...]
        </xs:schema>
    </types>
    <message name="SomeMessage">
        <part name="parameters" element="tns:SomeMessage"/>
    </message>
    <portType name="ServiceName">
    <operation name="SomeMessage">
        <input message="tns:SomeMessage" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <output message="tns:SomeMessageResponse" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <fault name="ServiceException" message="tns:ServiceException" wsam:Action="http://[domain]/[namespace]/[method]/fault/ServiceException"/>
    </operation>
    <xs:complexType name="ServiceException">
        <xs:sequence>
            <xs:element name="errorCode" type="xs:int"/>
            <xs:element name="message" type="xs:string" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
    [...etc...]
</definitions>
.
You may have noticed the additional 'xs:import' elements in the schema that we just inlined. These would have to be inlined as well. You would simply navigate to the schema location for each import, and then copy-paste the <schema> block for each import as peers to the schema we just copy-pasted, and remove the import elements, as follows, with the new schemas in bold-blue, and the removed import elements in strikethrough (e.g. strikethrough):
.
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="ServiceName" targetNamespace="http://[domain]/[namespace]" xmlns:tns="http://[domain]/[namespace]" [...etc...]>
    <types>
        <xs:schema xmlns:tns="http://[domain]/[namespace_1]" version="1.0" targetNamespace="http://[domain]/[namespace_1]">
            <xs:import namespace="http://[domain]/[namespace_2" schemaLocation="https://[domain]/[namespace_2]"/>
            <xs:import namespace="http://[domain]/[namespace_3" schemaLocation="https://[domain]/[namespace_3]"/>
            <xs:element name="SomeException" type="tns:SomeException]"/>
            <xs:element name="SomeService" type="tns:SomeService"/>
            [...etc...]
        </xs:schema>
        <xs:schema xmlns:tns="http://[domain]/[namespace_2]" version="1.0" targetNamespace="http://[domain]/[namespace_2]">
            [...etc...]
        </xs:schema>
        <xs:schema xmlns:tns="http://[domain]/[namespace_3]" version="1.0" targetNamespace="http://[domain]/[namespace_3]">
            [...etc...]
        </xs:schema>
    </types>
    <message name="SomeMessage">
        <part name="parameters" element="tns:SomeMessage"/>
    </message>
    <portType name="ServiceName">
    <operation name="SomeMessage">
        <input message="tns:SomeMessage" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <output message="tns:SomeMessageResponse" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <fault name="ServiceException" message="tns:ServiceException" wsam:Action="http://[domain]/[namespace]/[method]/fault/ServiceException"/>
    </operation>
    <xs:complexType name="ServiceException">
        <xs:sequence>
            <xs:element name="errorCode" type="xs:int"/>
            <xs:element name="message" type="xs:string" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
    [...etc...]
</definitions>
.
You need not get trapped in recursion during this process ;-) 
.
If, for example, the schema for namespace_3 imported namespace_2, then you can rely on the already copy-pasted namespace_2 in the Consolidated_WSDL. 
.

Step 2: Take Exception with 'Exception' 

.
The Apex compiler does not like it when a class ends with the keyword 'Exception' unless it extends the Exception Class. In the example above, you will notice the 'ServiceException' complex type, element, fault, etc. This would need to be renamed in an identifiable way, so that you can locate the renamed artifacts in the generated Apex later (see below). I simply appeneded 'WSDL' to the end of all such 'Exception' references in a rather large WSDL, taking care to replace "ServiceException" with "ServiceExceptionWSDL" and "tns:ServiceException" with "tns:ServiceExceptionWSDL", etc. Take care to be very specific about your find-and-replace operations, so as not to accidentially replace an 'Exception' reference in a URL. I suggest you use fully double-quoted strings in your find-and-replace logic, so that you avoid URLs altogether. 
.
The final Consolidated_WSDL would look something like this:
.
<?xml version="1.0" encoding="UTF-8"?>
<definitions name="ServiceName" targetNamespace="http://[domain]/[namespace]" xmlns:tns="http://[domain]/[namespace]" [...etc...]>
    <types>
        <xs:schema xmlns:tns="http://[domain]/[namespace_1]" version="1.0" targetNamespace="http://[domain]/[namespace_1]">
            <xs:element name="SomeExceptionWSDL" type="tns:SomeExceptionWSDL]"/>
            <xs:element name="SomeService" type="tns:SomeService"/>
            [...etc...]
        </xs:schema>
        <xs:schema xmlns:tns="http://[domain]/[namespace_2]" version="1.0" targetNamespace="http://[domain]/[namespace_2]">
            [...etc...]
        </xs:schema>
        <xs:schema xmlns:tns="http://[domain]/[namespace_3]" version="1.0" targetNamespace="http://[domain]/[namespace_3]">
            [...etc...]
        </xs:schema>
    </types>
    <message name="SomeMessage">
        <part name="parameters" element="tns:SomeMessage"/>
    </message>
    <portType name="ServiceName">
    <operation name="SomeMessage">
        <input message="tns:SomeMessage" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <output message="tns:SomeMessageResponse" wsam:Action="http://[domain]/[namespace]/[method]"/>
        <fault name="ServiceExceptionWSDL" message="tns:ServiceExceptionWSDL" wsam:Action="http://[domain]/[namespace]/[method]/fault/ServiceException"/>
    </operation>
    <xs:complexType name="ServiceExceptionWSDL">
        <xs:sequence>
            <xs:element name="errorCode" type="xs:int"/>
            <xs:element name="message" type="xs:string" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
    [...etc...]
</definitions>
.

Generate from WSDL

.
When you generate new Apex code from the WSDL, you will be given the opportunity choose the same Apex class name for all inlined namespaces, or name them differently. Whatever you do, I suggest you pick a short name or names, just based on my experience of rather lengthy names in the WSDL that got truncated when I just accepted the defaults.
.
After you generate the Apex, you will need to refactor any 'Exception' classes and references to back to the original name, so that the final Apex conforms with the service contract. In the example we've been using so far, the ServiceExeption complex type renamed to ServiceExceptionWSDL would result in an Apex class that looks something like this:
.
public class ServiceExceptionWSDL {
    public Integer errorCode;
    public String message;
    [...etc...]
}
.
You need only remove whatever suffix (or prefix, or whatever) you added to the name (in this case, 'WSDL') and have the class extend the 'Exception' interface, as follows:
.
public class ServiceException extends Exception {
    public Integer errorCode;
    public String message;
    [...etc...]
}
.
In my particular case, I replaced 100s of references to ServiceException with ServiceExceptionWSDL in my Consolidated_WSDL, but all of those references amounted to a single, generated Apex class, so the refactoring was easy. Your mileage may vary.
.

References

.
Mr. Brent Schreibfeder, a Salesforce colleague, who graciously helped me with my general lack of understanding of WSDL structure and the process to inline imports.
.
.

No comments:

Post a Comment