Consuming web services

gkubu - 2012-06-22 19:08:46 This page contains examples of how to use the „Web services for Tcl “ client side package. The package was created and is maintained by Gerald Lester, who also patiently provides support for ignorant users.

The client parses so-called wsdl files [L1 ]. From this file you get most of the information you need:

  • name of the web service
  • names of the operations it provides
  • parameters of the operations

You may also get the information from the documentation of the service, or by a tool like soapUI, or by the CreateStubs command of the package (see below).

The wsdl file contains the information in tags like

<wsdl:service name="country">
<wsdl:operation name="GetCountryByCountryCode">
<s:element name="GetCountryByCountryCode">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="CountryCode" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>

(The style here is "document/literal", refered to as "document/literal wrapped" in [L2 ]).

Even those not familiar with wsdl can certainly identify the elements in this example (from the wsdl file "http://www.webservicex.net/country.asmx?WSDL ")

  • the web service name is „country“
  • the operation name is „GetCountryByCountryCode“
  • the operation parameter is „CountryCode“ of type „string“

lm 2012/08/21 : Note that

set ret [::WS::Client::GetAndParseWsdl "http://www.webservicex.net/MortgageIndex.asmx?WSDL"]

returns a dictionary containing all the information describing the service. This dictionnary contains the keys name, operList (list of available operations) and operation (description of individual operations), t.

Example 1: simple synchronous information retrieval

package require Tcl 8.5
package require WS::Client

::WS::Client::GetAndParseWsdl "http://www.webservicex.net/MortgageIndex.asmx?WSDL"

# DoCall: yields result as a nested dict
set monthlyIndex [ ::WS::Client::DoCall MortgageIndex GetCurrentMortgageIndexMonthly {} ]

set key [ dict keys $monthlyIndex ]
flush stdout
puts "Key: $key"

foreach { i v } [ dict get $monthlyIndex $key ] { puts "$i $v" }

should return something like

Key: GetCurrentMortgageIndexMonthlyResult
IndexDate 7/1/2004
OneYearConstantMaturityTreasury 2.1
ThreeYearConstantMaturityTreasury 3.05
FiveYearConstantMaturityTreasury 3.69
ThreeMonthTreasuryBill 1.36
SixMonthTreasuryBill 1.69
ThreeMonthSecondaryMarketCD 1.4625
SixMonthSecondaryMarketCD -
EleventhDistrictCOFI 1.4929
CostOfSavingsIndex 1.6945
OneMonthLIBOR 1.9857
ThreeMonthLIBOR 2.4632
SixMonthLIBOR 1.1617
OneYearLIBOR 1.91
CostOfDepositsIndex 1.57
TwelveMonthTreasuryAverage 1.85

Variants

DoRawCall returns the xml response of the web service

::WS::Client::DoRawCall MortgageIndex GetCurrentMortgageIndexMonthly {}
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">  <soap:Body><GetCurrentMortgageIndexMonthlyResponse xmlns="http://www.webserviceX.NET/">
<GetCurrentMortgageIndexMonthlyResult>
<IndexDate>7/1/2004</IndexDate>
<OneYearConstantMaturityTreasury>2.1</OneYearConstantMaturityTreasury>
<ThreeYearConstantMaturityTreasury>3.05</ThreeYearConstantMaturityTreasury>
<FiveYearConstantMaturityTreasury>3.69</FiveYearConstantMaturityTreasury>
<ThreeMonthTreasuryBill>1.36</ThreeMonthTreasuryBill>
<SixMonthTreasuryBill>1.69</SixMonthTreasuryBill>
<ThreeMonthSecondaryMarketCD>1.4625</ThreeMonthSecondaryMarketCD>
<SixMonthSecondaryMarketCD>-</SixMonthSecondaryMarketCD>
<EleventhDistrictCOFI>1.4929</EleventhDistrictCOFI>
<CostOfSavingsIndex>1.6945</CostOfSavingsIndex>
<OneMonthLIBOR>1.9857</OneMonthLIBOR>
<ThreeMonthLIBOR>2.4632</ThreeMonthLIBOR>
<SixMonthLIBOR>1.1617</SixMonthLIBOR>
<OneYearLIBOR>1.91</OneYearLIBOR>
<CostOfDepositsIndex>1.57</CostOfDepositsIndex>
<TwelveMonthTreasuryAverage>1.85</TwelveMonthTreasuryAverage>
</GetCurrentMortgageIndexMonthlyResult>
</GetCurrentMortgageIndexMonthlyResponse>
</soap:Body>
</soap:Envelope>

If you don't want to look up the name of the web service, you can provide your own. However, if you want to discuss the service, it might be advantageous to use its proper name.

package require WS::Client
::WS::Client::GetAndParseWsdl "http://www.webservicex.net/MortgageIndex.asmx?WSDL" {} mogaIxAlias
set monthlyIndex [ ::WS::Client::DoCall mogaIxAlias GetCurrentMortgageIndexMonthly {} ]
puts [ dict get $monthlyIndex GetCurrentMortgageIndexMonthlyResult TwelveMonthTreasuryAverage ]
1.85

As mentioned above, the CreateStubs command returns operations and parameters

package require WS::Client
::WS::Client::CreateStubs MortgageIndex                  ;# || ::WS::Client::CreateStubs mogaIxAlias

        ::MortgageIndex::GetCurrentMortgageIndexMonthly {}
        ::MortgageIndex::GetCurrentMortgageIndexByWeekly {}
        ::MortgageIndex::GetMortgageIndexByMonth {Month Year}
        ::MortgageIndex::GetMortgageIndexByWeek {Day Month Year}

The stubs are actually commands, which can be used instead of DoCall (a stub is a wrapper for DoCall), but they are missing the flexibility of the latter.

::MortgageIndex::GetMortgageIndexByMonth 5 2004

Example 2: http headers

Some web services require additional information in http headers, e.g. for authentification. This is shown for a call to a simple eBay web service. You will need an eBay developer account, if you want to reproduce the example [L3 ]. Information on eBay web services is available on [L4 ].

The eBay web service GeteBayTime requires an application ID, an API call name, a site ID and the API version as http header fields. WS::Client takes them as a key-value list.

The http headers are supplied as the fourth parameter to the DoCall and DoRawCall commands. Stubs cannot take such a parameter.

package require Tcl 8.5
package require WS::Client 2.2.7

dict set httpParm X-EBAY-API-APP-ID           abcdefgh-1a23-45b6-789c-d012345ef6a7  ;# invalid ID - for demonstration only
dict set httpParm X-EBAY-API-SITE-ID          77
dict set httpParm X-EBAY-API-CALL-NAME        GeteBayTime
dict set httpParm X-EBAY-API-VERSION          775

::WS::Client::GetAndParseWsdl "http://developer.ebay.com/webservices/latest/ShoppingService.wsdl"

::WS::Client::DoRawCall Shopping GeteBayTime {} $httpParm

DoCall needs another option, as will be demonstrated below. DoRawCall yields

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <soapenv:Body>
  <GeteBayTimeResponse xmlns="urn:ebay:apis:eBLBaseComponents">
   <Timestamp>2012-06-23T18:02:51.683Z</Timestamp>
   <Ack>Success</Ack>
   <Build>E777_CORE_BUNDLED_14948543_R1</Build>
   <Version>775</Version>
  </GeteBayTimeResponse>
 </soapenv:Body>
</soapenv:Envelope>

Example 3: Configuration

The wsdl includes the URI of the web service. You may want to change it, e.g. for test purposes. The ::WS::Client::Config command provides for that:

::WS::Client::GetAndParseWsdl "file://localhost:P:/edv/programmierung/tcl.tk/div/ebayShopping.wsdl"

::WS::Client::Config Shopping location http://open.api.sandbox.ebay.com/shopping

dict set httpParm X-EBAY-API-APP-ID        abcdefgh-1a23-45b6-789c-d012345ef6a7
dict set httpParm X-EBAY-API-SITE-ID       77
dict set httpParm X-EBAY-API-CALL-NAME     GeteBayTime
dict set httpParm X-EBAY-API-VERSION       775

::WS::Client::DoRawCall Shopping GeteBayTime {} $httpParm

Now let's try DoCall:

::WS::Client::DoCall Shopping GeteBayTime {} $httpParm
# no response, therefore ...

::WS::Client::Config Shopping skipLevelWhenActionPresent 1
::WS::Client::DoCall Shopping GeteBayTime {} $httpParm
GeteBayTimeResponse {Ack Failure Errors {{ShortMessage {Ungültige Eingabedaten.} LongMessage {Die Eingabedaten für diesen Tag sind ungültig oder fehlen. Bitte lesen Sie die API-Dokumentation.} ErrorCode 1.22 SeverityCode Error ErrorParameters {{Value {Body not found.}}} ErrorClassification RequestError}} Build E777_CORE_BUNDLED_14948543_R1 Version 777}

We got a response, but this is not what we wanted. The error message says "Value {Body not found.}". There are two ways to overcome this:

# supply the SOAP body
dict set body GeteBayTimeRequest ""

# note the parameter "$body"
::WS::Client::DoCall Shopping GeteBayTime $body $httpParm
GeteBayTimeResponse {Timestamp 2012-06-24T14:19:08.092Z Ack Success Errors {{} {} {} {}} Build E777_CORE_BUNDLED_14948543_R1 Version 777}
# the stub handles this alike: ::Shopping::GeteBayTime GeteBayTimeRequest

# Alternative (WS::Client version 2.2.8 required)
::WS::Client::Config Shopping skipLevelWhenActionPresent 0
::WS::Client::Config Shopping skipLevelOnReply 1
::WS::Client::DoCall Shopping GeteBayTime {} $httpParm
GeteBayTimeResponse {Timestamp 2012-06-24T14:24:46.699Z Ack Success Errors {{} {} {} {}} Build E777_CORE_BUNDLED_14948543_R1 Version 777}

When creating the dictionary for the result from the <body> part of the SOAP response, skipLevelonReply causes the routine to skip one level of the DOM tree hierarchy, skipLevelWhenActionPresent does so if the result of the first level is empty. A look at proc ::WS::Client::parseResults in ClientSide.tcl should make this clear.

Should you want to use stubs, you will have to prepare your environment because of the need for http header fields.

package require WS::Client 2.2.8

set preparsed [ ::WS::Client::GetAndParseWsdl "http://developer.ebay.com/webservices/latest/ShoppingService.wsdl" ]
flush stdout

dict set httpParm X-EBAY-API-APP-ID        abcdefgh-1a23-45b6-789c-d012345ef6a7
dict set httpParm X-EBAY-API-SITE-ID       77
dict set httpParm X-EBAY-API-CALL-NAME     GeteBayTime
dict set httpParm X-EBAY-API-VERSION       775

set serviceName [ WS::Client::LoadParsedWsdl $preparsed $httpParm ]

::WS::Client::Config $serviceName location "http://open.api.sandbox.ebay.com/shopping"
::WS::Client::Config $serviceName skipLevelOnReply 1

::WS::Client::CreateStubs $serviceName

# $serviceName is "Shopping", of course
::${serviceName}::GeteBayTime GeteBayTimeRequest
GeteBayTimeResponse {Timestamp 2012-06-27T20:52:19.092Z Ack Success Errors {{} {} {} {}} Build E779_CORE_BUNDLED_14991381_R1 Version 779}

Example 4: submitting parameters

While for the stub the parameters were submitted by position (see example 1), for DoCall we need a list or a dict.

package require Tcl 8.5
package require WS::Client

::WS::Client::GetAndParseWsdl "http://www.webservicex.net/MortgageIndex.asmx?WSDL"

# remember the definition: GetMortgageIndexByMonth {Month Year}
dict set monthYear Month 4
dict set monthYear Year 2004

::WS::Client::DoCall MortgageIndex GetMortgageIndexByMonth $monthYear

# Alternative
::WS::Client::DoCall MortgageIndex GetMortgageIndexByMonth {Month 4 Year 2004}
# also
::WS::Client::DoCall MortgageIndex GetMortgageIndexByMonth {Year 2004 Month 4 }

Example 5: Local wsdl file

package require WS::Client

set secrets "somewhereOnYourSystem"
::WS::Client::GetAndParseWsdl "file://$secrets/mortgageIndex.wsdl"

# Alternative

package require WS::Client

set secrets "somewhereOnYourSystem"
set fdWsdl [ open [ file join $secrets MortgageIndex.wsdl ] r ]
set wsdl [ read $fdWsdl ]
close $fdWsdl

::WS::Client::ParseWsdl $wsdl

Example 6: Using a transform to inject a soap header

An input transform like the following is possible after application of this one-line bugfix which seems to already be in trunk .

#
# extending the MortgageIndex example above to insert a (here, useless) soap header
#

package require WS::Client

set preparsed [::WS::Client::GetAndParseWsdl "http://www.webservicex.net/MortgageIndex.asmx?WSDL"]

set svcName [WS::Client::LoadParsedWsdl $preparsed {}]
WS::Client::CreateStubs $svcName

#
# a quick and very dirty way to inject SOAP-ENV:Header
#
proc xformInAddHeader {svcName opName tType xml {url {}} {argList {}}} {
  set hdr {<SOAP-ENV:Header><!-- stuff here --></SOAP-ENV:Header>}
  if {[string equal $tType "REQUEST"]} {
    set xml [string map [list <SOAP-ENV:Body> $hdr<SOAP-ENV:Body>] $xml]
  }
  return $xml
}
WS::Client::SetServiceTransforms $svcName xformInAddHeader

puts [${svcName}::GetCurrentMortgageIndexMonthly]

See also

Webservices

http://core.tcl.tk/tclws/wiki?name=WSClient+Client+Side

Other techniques

TclSOAP (note the comment of Pat Thoyts regarding maintenance for this package)

NOAA Weather Forecast


tommycat - 2012-08-19 18:59:07

Where do we get tclws 2.2.8 as referenced in the examples above? The highest I'm seeing is 2.2.7


gkubu - 2012-08-20 17:15:27

2.2.8 is the trunk currently [L5 ]. It's needed for the skipLevelOnReply option. Accidentally I've just noticed that ActiveStates teapot archive includes 2.2.8 already.


ALX 2016-03-17 10:18:00

Simple HTTP Authentication Wrapper for http::geturl RFC 2617

HaO: Alex, if you have put the link here, could you also add information how to use it with tclws client ? Please remove this remark if wrong or done. Thanks.

You may also include your solution to the manual solution on page TCLWS (Web Services for Tcl) as SAP Client.