Using Static WSDL with Service Now

The static WSDL has always been a bit of a mystery to me. I have worked with them in standard integration offerings with ServiceNow, but never created one from scratch. I decided to have a go at it so that I could fully understand where all of the puzzle pieces fit in this crazy thing called the “Static WSDL”.

To do this, I decided to create a web service that provides the user with a WSDL that explains what they can do with my web service. This web service is going to fake out a Stock Quote request system. If someone requests a stock quote, we are going to respond to them with a message that gives them the username that they are using for the SOAP call and we are going to respond back with their Symbol. Nothing more.

The Scripted Web Service

There are really two parts to using a Static WSDL. The first requirement is the creation of a Scripted Web Service. You can read more about them on the Scripted Web Service documentation page.

My scripted web service that I created, looks like the following:

Scripted Web Service

FakeStockValue - Scripted Web Service

The WSDL Field on the scripted web service is auto generated based on the name of my scripted web service record. This is the WSDL value that you will provide to your SOAP consumer client. When they make SOAP calls into your web service, it will execute the script in the script field. I will get to the script in a moment. But first, if the client were to download the WSDL associated with this web service at this point, they are going to get a generic WSDL that looks like the following document:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions targetNamespace="http://www.service-now.com" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:tns="http://www.service-now.com/Test123" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:sncns="http://www.service-now.com" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
  <wsdl:types>
    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified" targetNamespace="http://www.service-now.com/Test123"><xsd:element name="execute"><xsd:complexType><xsd:sequence/></xsd:complexType></xsd:element><xsd:element name="executeResponse"><xsd:complexType><xsd:sequence/></xsd:complexType></xsd:element></xsd:schema>
  </wsdl:types>
  <wsdl:message name="executeSoapIn">
    <wsdl:part name="Test123" element="tns:execute"/>
  </wsdl:message>
  <wsdl:message name="executeSoapOut">
    <wsdl:part name="Test123" element="tns:executeResponse"/>

  </wsdl:message>
  <wsdl:portType name="ServiceNowSoap">
    <wsdl:operation name="execute">
      <wsdl:input message="sncns:executeSoapIn"/>
      <wsdl:output message="sncns:executeSoapOut"/>
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="ServiceNowSoap" type="sncns:ServiceNowSoap">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>

    <wsdl:operation name="execute">
      <soap:operation soapAction="http://www.service-now.com/Test123/execute" style="document"/>
      <wsdl:input>
        <soap:body use="literal"/>
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal"/>
      </wsdl:output>
    </wsdl:operation>

  </wsdl:binding>
  <wsdl:service name="ServiceNow_Test123">
    <wsdl:port name="ServiceNowSoap" binding="sncns:ServiceNowSoap">
      <soap:address location="https://myinstance.service-now.com/Test123.do?SOAP"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

The Static WSDL

This is where the Static WSDL comes into play. If you were to navigate to System Web Services -> Static WSDL, you will be presented with a list of Static WSDL records.

Navigation to the Static WSDL

Navigation to the Static WSDL

One of the main purposes of the Static WSDL feature is that some clients require a WSDL in a certain format for them to work properly. The standard ServiceNow format will not work with the consumer. In this case you can create the WSDL in the format required and still do whatever you need to do given that data submitted through the SOAP Web Service.

In order to override the standard WSDL for your scripted web service, you need to create a Static WSDL record that has the same name as your Scripted Web Service. Create it and populate the WSDL field with your custom WSDL.

This is what mine looks like:

My custom static WSDL

The Custom Static WSDL record I created

Now, the WSDL document can be anything, it just needs to be in WSDL format, and then you just need to work within that WSDL format with your Script on the Scripted Web Service. I just grabbed a sample WSDL from the internet for stock quotes. My actual WSDL XML is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0"?>
<definitions name="StockQuote"
             targetNamespace="http://example.com/stockquote.wsdl"
             xmlns:tns="http://example.com/stockquote.wsdl"
             xmlns:xsd1="http://example.com/stockquote.xsd"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns="http://schemas.xmlsoap.org/wsdl/">

  <types>
    <schema targetNamespace="http://example.com/stockquote.xsd"
            xmlns="http://www.w3.org/2000/10/XMLSchema">
      <element name="TradePriceRequest">
        <complexType>
          <all>
            <element name="tickerSymbol" type="string"/>
          </all>
        </complexType>
      </element>
      <element name="TradePrice">
         <complexType>
           <all>
             <element name="price" type="float"/>
           </all>
         </complexType>
      </element>
    </schema>
  </types>

  <message name="GetLastTradePriceInput">
    <part name="body" element="xsd1:TradePriceRequest"/>
  </message>

  <message name="GetLastTradePriceOutput">
    <part name="body" element="xsd1:TradePrice"/>
  </message>

  <portType name="StockQuotePortType">
    <operation name="GetLastTradePrice">
      <input message="tns:GetLastTradePriceInput"/>
      <output message="tns:GetLastTradePriceOutput"/>
    </operation>
  </portType>

  <binding name="StockQuoteSoapBinding" type="tns:StockQuotePortType">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <operation name="GetLastTradePrice">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>

  <service name="StockQuoteService">
    <documentation>My first service</documentation>
    <port name="StockQuotePort" binding="tns:StockQuoteSoapBinding">
      <soap:address location="https://myinstance.service-now.com/FakeStockValue.do?SOAP"/>
    </port>
  </service>

</definitions>

Please note, for the most part I left my WSDL the same as the sample version. I did change the 6th to last line to be the real SOAP endpoint for this web service: “https://myinstance.service-now.com/FakeStockValue.do?SOAP”. This is the same url as the WSDL URL but I replace WSDL with SOAP.

The script

Now, that we have an overriding static WSDL for our scripted web service, we will focus on the script we used for the scripted web service record. This is the script that defines what we are going to do in the system when a SOAP call is made to this endpoint.

In order to keep things clean, I am going to call a Script Include in my script. Here is the script as it calls my script include:

1
2
3
4
5
6
7
8
var vProcessor = new FakeStockQuote(soapRequestXML);

var responseElement = vProcessor.process();
if (responseElement != null) {
  response.soapResponseElement = responseElement;
} else {
  response.soapResponseElement = vProcessor.generateSoapFault("unknown error");
}

When we get a SOAP request into our Scripted Web Service, the XML SOAP request is stored in a variable called “soapRequestXML”.

We will take that XML string and pass it into our FakeStockQuote class. That class will parse the XML and then allow us to process it. When we process it, if everything goes as we expect, our Script Include returns an XML response element that I am storing in the “responseElement” variable. Otherwise, we are going to sent an XML blob back saying we had an unknown error.

The Script Include

Most of the work for my demonstration will take place in the FakeStockQuote script include.

Here is the code for the Script Include:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var FakeStockQuote = Class.create();

FakeStockQuote.prototype = {
  initialize : function(requestXML) {
    //Use some backend XML utilities...you could use string tools if you wish
    this.xmlutil = Packages.com.glide.util.XMLUtil;
    //converting the string to an XML Document
    this.fSoapDoc = new XMLDocument(requestXML);
  },

  process : function() {
    var soapBody = this.fSoapDoc.getNode("/Envelope/Body");
    //Our WSDL was formatted to have the only first child element be the function
    var funcNode = this.xmlutil.getFirstChildElement(soapBody);
    var nodeName = this.xmlutil.getNodeNameNS(funcNode);

    //If the function for this SOAP request is TradePriceRequest, let's do our funny stuff
    if (nodeName == "TradePriceRequest") {
      return this.fakeOutTradePriceRequest(funcNode);
    }
   
    //Couldn't find any supported functions in this SOAP request
    return this.generateSoapFault("un-supported API call: " + nodeName);
  },

  fakeOutTradePriceRequest : function (funcNode) {
    //Create the beginnings of our XML response
    var r = new XMLDocument("<GetLastTradePriceOutput xmlns='https://www.service-now.com/vws/FakeStockValue'/>");

    //Do some funny stuff...were going to get the USER ID of the user
    //used to make this SOAP call.  Then we will return the
    //stock symbol they were asking about
    var usersysid = gs.getUserID();
    var gr = new GlideRecord("sys_user");
    gr.get(usersysid);
    var username = gr.user_name;
    var quoteSymbol = this.xmlutil.getText(funcNode);
    //Create a "message" element to store our response message
    r.createElement("message", username + ", You were looking for a quote on "+quoteSymbol);
    return r.getDocumentElement();
  },


  generateSoapFault : function (str) {
     var f = "<SOAP-ENV:Fault>" +
               "<faultcode xsi:type='xsd:string'>SOAP-ENV:FakeStockValue</faultcode>" +
               "<faultstring xsi:type='xsd:string'>" + str +
               "</faultstring>" +
             "</SOAP-ENV:Fault>"
     var s = new XMLDocument(f);
     return s.getDocumentElement();
  }
}

I tried to comment this code a bit, but let me give a quick overview what it does.

Initialize

The initialize function just takes the xml request string and converts it to an XML Document object that we can use libraries to traverse and manipulate. You could just as easily keep it a string and use regular expressions to navigate it if you are more comfortable with that.

Process

The “process” function is what we called in our Scripted Web Service script. What it is doing is grabbing the first child element in the XML after the body element. Our WSDL that we used has that first child element be the element that tells us which function to use. In this WSDL we only had one function possible, but most WSDL’s provide many functions that are available. If that were the case in this situation, we would have more “if” statements that tested the first child element node for the various function names. Right now, we just check for the only one we have available. If it is not the correct one, we through an error.

fakeOutTradePriceRequest

This is the implementation of our only available function in the WSDL, which was “TradePriceRequest”. This is what we called in the “if” statement in the “process” function. What I did here was have it look up the user that the SOAP request authenticated as and retrieve the user_name. Then I just built out an XML document that creates a message and returns it back to the SOAP client.

If this were a useful example, we would have done some actual work, like look up a stock symbol and provide the last traded price. But this is a demo, so what do you expect?

generateSoapFault

This function just returns a SOAP error that we can call if there are problems.

The example in action

So, now, if you were to go to a SOAP client such as SOAP UI and load our WSDL, you would get the following function available to you:

WSDL Loaded

The Functions available in SOAP UI when the WSDL is loaded

Now, change the default XML in the request to include a Stock Symbol:

1
2
3
4
5
6
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:stoc="http://example.com/stockquote.xsd">
   <soapenv:Header/>
   <soapenv:Body>
      <stoc:TradePriceRequest>IBM</stoc:TradePriceRequest>
   </soapenv:Body>
</soapenv:Envelope>

If you submit that, our sample web service will return the following XML repsonse:

1
2
3
4
5
6
7
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <SOAP-ENV:Body>
      <GetLastTradePriceOutput xmlns="https://www.service-now.com/vws/FakeStockValue">
         <message>admin2, You were looking for a quote on IBM</message>
      </GetLastTradePriceOutput>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>