- 1 SOAP WSDL Package
- 2 Basic Usage
- 3 WSDL Class Generator
- 4 Example: PartnerService
- 5 SOAP Package Structure
- 6 Classes of Special Interest
SOAP WSDL Package
These chapter describes the basic underlying implementation. The code generated by the WSDL class generator calls those functions.
To make a SOAP request as a client, given a WSDL (either as file, string or URL), you have to:
- create a service from the WSDL
- instantiate a client
- create a call object with call arguments
- perform the call
- extract the values from the returned result object
Creating a Service Instance
The service instance keeps the information about the available entry points, the protocol and encoding and the data types. You need a WSDL to create a service. This can come from multiple sources:
From URL (preferred)
this is the preferred method, especially as it will automatically deal with imported schema definitions (i.e. if the WSDL refers to other documents via an import)
service := SprayWSDLService onUrl: 'anUrlString'
From a String
this only works, if the WSDL is self contained (eg. contains all required definitions and does not import other documents). If it does, missing documents will be fetched automatically via HTTP (see description on transports below).
service := SprayWSDLService onXmlString: 'wsdlString'
From a Set of Files/Strings
when a WSDL URL is parsed, a transport object instance is used to fetch imported documents. By default, an instance of SptHTTPClient is used to fetch required documents.
A mock transport class named SptHTTPLocalTransport can be used. This keeps a list of string documents in a dictionary mapped by URL and delivers that contents, when asked for (i.e. it simulates an SptHTTPClient-transport).
Thus, if you have a WSDL document, which imports other documents, AND you want to prevent WSDL fetches via HTTP of your program at runtime, you should setup an instance of SptHTTPLocalTransport, give it all the documents (incl. any imported docs), and provide that as a document retriever.
For example, if you have a WSDL in (urlA), which imports urlB and urlC, create a transport which contains all three documents as:
localTransport := SptHTTPLocalTransport new. localTransport localDocuments at: urlA "eg something like: 'http://foo.services.de:30050/partner/PartnerBusinessService?SCHEMA'" put: '<?xml version="1.0"?> ... the whole urlA document as string... '. localTransport localDocuments at: urlB "eg something like: 'http://foo.services.de..." put: '<?xml version="1.0"?> ... the whole urlB document as string... '. localTransport localDocuments at: urlC "eg something like: 'http://foo.services.de..." put: '<?xml version="1.0"?> ... the whole urlC document as string... '.
and then create the service with:
definitions := WSDLDefinitions onUrl: urlA transport: localTransport. service := SprayWSDLService onDefinitions: definitions.
In such a setup, all required import URLs will be fetched from there (e.g. no HTTP requests required).
You can provide the URL contents from class variables, class getter methods, etc. Of course, you can also read the documents from a local file, and setup the localTransport instance from their contents.
From Binary Storage
All of the above methods require some initial processing time (in the order of a few 100ms), because they read the textual WSDL with the XML parser, and construct a rather complicated object structure containing those definitions. (most of the space is taken up by XML-schema definitions, which are kept as XML-node trees).
The original Dolphin implementation used Smalltalk binary object storage to speed this up. Definition objects were converted to a binary store bytes at development time, those kept in a class variable in the deployed end-user image and restored from it at execution time.
Be aware, that binary storage data is both system dependent and hard to analyze in case of errors. Our experience is that it is worth to have the WSDL around in plain text, as fixes are much easier to make there, in case of problems.
Instantiating a Client
Once you have the service object, you need a client to a concrete partner service. This client prepares the outgoing and incoming envelopes, to save some processing time in the actual message send.
To create a client, use either:
client := service createClient
which creates a client to the service's default host (which is the one specified in the WSDL).
Often, this is not the one you want to connect to (for example, if you have a local test service running, and/or the host address of the service is different for in-house connections). Also, some WSDLs do not include a valid service URL.
client := service createClientTo:'http://foo.bar.com:4933/bla/...'.
Create a Call Object with Call Arguments
Finally, an actual service call is to be performed.
This is a two-step operation: first, a call instance is created, which takes the name of the operation and its arguments, and generates an out-envelope (which includes the XML representation of the arguments, as specified in the WSDL's encoding information). The second step is the actual call, which is described below.
Call argument objects fall into two categories:
- simple objects (xsd:simpleTypes, which can be directly mapped to Smalltalk objects)
- complex objects (in the XML schema, these are xsd:complexType elements)
Simple types are mapped to corresponding Smalltalk objects:
- xsd:string - String
- xsd:int - Integer
- xsd:integer - Integer
- xsd:boolean - Boolean
- xsd:date - Date
- xsd:time - Time
There are a few xsd:simpleTypes, for which no standard Smalltalk object exists on all machines (xsd:datetime). Depending on the dialect, these are either mapped to existing classes or to classes from the SOAP framework. For example, xsd:datetime is mapped to the Timestamp class in Smalltalk/X but to SOAP__XeXSDDateTime in VSE.
Complex types must be represented as instances of one of:
- an XeQstruct - this is a special kind of Dictionary, which uses qualified names (e.g. XML names with namespace and prefix) as keys. This is the default representation used by the SOAP framework.
- a Dictionary - if values are given as dictionary instance, it must contain key-value mappings for the sub-elements. The keys are the localName of the schema's element names (i.e. if the element is named foo:someType, then the key should be 'foo' only).
- a special data holder object - this must understand getter- and setter messages, corresponding to the local names of the elements.
A concrete example is found in the BLZ-testcase:
call := client send: 'getBank' withArguments:getBankArg.
Performing the Service Call
Finally, execute the call by giving it a #value message (think of the call object as a block, which does the call for you):
result := call value.
this harmless looking message performs the transfer. It sets up the communication (usually HTTP, where the SOAP message is wrapped into an HTTP-request), sends the message in an asynchronous send process, awaits the response, decodes the returned XML and instantiates the result object(s).
Notice that SOAP allows for multiple return values to be specified in the WSDL. If this is the case, the result returned by #value is the first return value, and a sequenceable collection of all return values is retrieved via #outParameters.
Debugging / Logging HTTP Traffic
Take a look at "UserPreferences" and the SOAP__SptHTTPRequest's Verbose class variable. See the chapter at the end on logging flags.
A call object's in/out XML can be inspected with:
call in xml inspect. call out xml inspect.
WSDL Class Generator
The wsdl class generator takes WSDL definitions and generates a client class containing entries for the operations, and type classes which hold the arguments and return values.
First, instantiate the WSDLClassBuilder.
You need either a service instance (see above) or a WSDL definitions object, as argument to the instantiation message.
service := SprayWSDLService onXmlString:'... a lot of WSDL here ...' builder := WSDLClassBuilder service: service
definitions := WSDLDefinitions onURL:'http://.....?wsdl'. builder := WSDLClassBuilder definitions: definitions
Then let it generate the classes with:
Unless overwritten by the setup messages below, the generated client class will have a name constructed from the service name in the WSDL. It will have a "SOAP__" prefix, followed by the service name and "Client" (i.e. "SOAP__<service name>Client"). For example, if the DeBeKa partnerservice-WSDL is used, the client class name would be "SOAP__PartnerServiceClient".
Data objects are generated for all non-simpleType objects, which are recursively reached/used by the WSDL operations, starting with the individual operation arguments. Datatype class names are constructed in a similar fashion: a "SOAP__" prefix, the service name, an underline, and the type name from the XSD-schema. For example, if the WSDL contains an XSD type named "bestaetigePartner", the generated type class would be named "SOAP__PartnerService_bestaetigePartner". The service name is included, to avoid a name conflict with type classes from other services.
The builder's naming defaults can be overwritten via setup methods described below. This must of course be done before calling the #generate method.
defines (overwrites) the name of the generated client class. Thus, if you say:
builder clientClassName: 'DBK_PService'.
the generated client class will be named "DBK_PService" instead of "SOAP_PartnerServiceClient"
defines (overwrites) the prefix to be used for type classes. This includes the namespace prefix. Thus, if you say:
builder typeClassNamePrefix: 'DBK_'.
all generated type classes will be named "DBK_<schema name>".
defines (overwrites) the prefix to be used for all classes (i.e. type- and client classes). This includes the namespace prefix. Thus, if you say:
builder classNamePrefix: 'DBK_'.
all generated type classes will be named "DBK_<schema name>", and the client class will be "DBK_<serviceName>Client".
Generated Data Holder Classes (TypeClasses)
For each used XSD type, a corresponding Smalltalk class is created. These are (and should be) complete passive objects which exist only as data holders. Their instance variable structure and getter/setter interface directly corresponds to the element-structure of the corresponding XSD types.
However, all element names are converted to lowercaseFirst (unless the forceLowercaseFieldNames option is set to false, as described below). Also, multiple occurs fields are given a name with a trailing underscore ("_"), and an additional adder-method is generated. I.e. for a multiple-occurs element named "foo", a getter named "foo_" (which returns the multiple elements as an array), a setter named "foo_:", which expects the multiple values as an array), and an adder "add_foo:" (which expects a single object to be added) are generated.
The translation to lowercase is done, because most Smalltalks give a warning about or even forbid instance variables with an uppercase first character. However, you may prefer having the original XSD schema names as instance variables and getter/setter names. Then use:
builder forceLowercaseFieldNames: false
before calling the generate method.
Using Generated Client Classes
The client class is generated as a subclass of SOAP__SprayWSDLService. It has to be instantiated, and contains a number of helper methods and the SOAP operation API in a category named "operations". Each operation expects a request-argument object as parameter, as specified in the WSDL. For example, in the partnerService, you will find methods named "bestaetigePartner:", "listPartner:" etc.
The generated methods are written to include comments and naming hints (argument names) which will help in instantiating the required argument objects correctly.
The default destination URL is returned by the "destinationUrl" method. By default, this returns the URL as found in the original WSDL. A setter method "destinationUrl:" allows for this default to be overwritten.
To instantiate a service client, use:
serviceClient := <generatedServiceClientClass> new.
to use a different URL, change it with:
serviceClient destinationUrl: 'http://...'.
Once instantiated, the serviceClient can be reused for multiple requests to the same service host. It does not keep connections open between service calls (unless your underlying HTTP interface does so, and the HTTP connection was opened with the "connection: keep" option - which is not done by default).
Then setup the service arguments. Look into the source of the client classes' operation methods, to see what it expects, and the source of the corresponding type-class to see what elements it has:
arg := <operationTypeClass> new. arg foo: valueForFoo. arg bar: valueForBar. ...
Finally, perform the service call with:
rslt := serviceClient operation: arg.
the returned result-object will be an instance of the response type-class, with getters as specified in the WSDL. Look at the comments in the getters, to see what you get.
Instantiating Argument Objects
The generated data holding type classes will have getters and setters for their elements. If the schema includes restrictions (such as a length restriction on a string, or an enumeration of allowed values), the setters will also perform verification of the argument. In addition, the generated code includes comments, which describe any restrictions.
The verification can be disabled via a central flag, as returned by: "SOAP__SoapSetting verifySoapCallArguments". You can change this method to return false for deployed images.
Accessing the Last Call Object (after a service call)
The generated client class has an instance variable, which holds the previous request object. This can be inspected in case of an error. For example,
serviceClient lastRequest out xml
returns the last out-going request's full XML envelope,
serviceClient lastRequest in xml
contains the last response.
Generating the PartnerService classes
The following code example is also found in "WSDLClassBuilderTest >> testClassBuilder_02".
Generating the partnerService classes:
definitions := WSDLDefinitions onUrl: 'http://plan-eval-esb.services.debeka.de:30050/partner/proxyservice/PartnerBusinessService?WSDL'. builder := WSDLClassBuilder definitions:definitions. builder generate.
this will generate the following classes (into the class category "SOAP-Generated-partnerService"):
To call the partnerService at its default service port address, use:
service := SOAP__PartnerService new. suchArg := SOAP_PartnerService_seitenSuche new. suchArg ergebnisType:'ALLES'. result := service listePartner: suchArg. "result will be an instance of kontrollTyp" Transcript show: result code. Transcript show: result fehler. Transcript show: result information.
for a different service URL (in-house test service), use:
service := SOAP__PartnerService new. service destinationUrl:'http://testServer:8080/partner/proxyservice/PartnerBusinessService. ...
SOAP Package Structure
The whole package is structured into a number of sub-packages, of which some are usable outside the SOAP context. Each package has a subpackage (named "XX_test" in ST/X and usually "XXTES" in VSE).
In ST/X, all SOAP classes live in a separate namespace ("SOAP"), and thus have a "SOAP::" prefix in their name. Inside the namespace, the prefix is not needed to refer to a class which is in the same namespace. Therefore, in ST/X code, you will find class references without prefix.
For VSE, which has no namespaces, class names are translated by prefixing the name with "<namespace>__". Thus all classes will have a "SOAP__" prefix in VSE. This is automatically done by the STX-VSE class exporter, which was developed specifically for this purpose.
Notice, that this automatic rewrite can of course not detect situations where class names are constructed dynamically, and referred to via "Smalltalk at:". Such code had to be identified and manually changed to deal with both dialects. Usually, you will find code like "Smalltalk isSmalltalkX ifTrue:[...]" or "Smalltalk isVisualSmalltalkEnterprise ifTrue:[...]" in those parts. The goal is to keep a common code base, so that future improvements and fixes will be immediately available in both dialects.
Package: xe ("stx_goodies_soap_xe")
contains classes to represent qualified names, XSD schema definitions, XSD typespaces and mappings etc. there are no input/output or GUI related operations in this package. Most of the classes found there can be mapped one-by-one to corresponding elements from an XSD schema definition. Object hierarchies of them are created by the WSDL readers, the SOAP envelope creators and the SOAP encoders.
Package: spray ("stx_goodies_soap_spray")
the basic SOAP framework. Includes both client and server code (however, the server part was not part of the VSE porting effort, and may not run without further debugging). The spray package does not need a WSDL. In theory, it is possible to setup the above described service/client/call objects manually, by appropriate instance creation and initialization messages. In practice, the setup is too complicated, and the WSDL subpackage does this for you.
Package: wsdl ("stx_goodies_soap_wsdl")
this contains classes to represent the information inside a WSDL, and to create a service object from it. Most of the code is in WSDLDefinitions, which deals with all xsd-schema handling, especially with the import of other documents (using transport objects).
Package: yaxo ("stx_goodies_xml_yaxo")
the YAXO ("yet another xml o-stands-for-whatever") XML parser. Reads XML and generates a hierarchy of XML nodes. This parser lives in its own namespace called "YAXO"; thus, in VSE, all of its classes will have a "YAXO__" prefix.
The SOAP framework is prepared to use different XML parsers, and uses adaptors which transforms the different XML nodes into its own XeNode representation. Currently three adaptors are present, for YAXO, VisualWorks XML parser and ExoboxXML parser. XML parsing speed is crucial when WSDLs are read and to decode SOAP envelopes. If performance ever becomes a major issue, you may replace this by a high speed XML parser (written in C), and create a matching adaptor (take SOAP::SoYaxXMLParserAdapter as a base).
Package: soap ("stx_goodies_soap")
contains all the rest, such as error classes, abstract classes, encoders/decoders, transport interfaces (adapters to HTTP), etc.
Package: auth ("stx_goodies_soap_spray_auth")
additional classes which deal with authentication. This is currently not maintained, and provided "as-is". No effort has been made in porting or verifying its usefulness. However, it seems to compile (at least) in VSE. Exept does not currently use these classes in any of its projects - we recommend using regular SOAP over a secure SSH (HTTPS) connection. Then this package will not be needed at all.
Classes of Special Interest
performs the actual HTTP get operation for both WSDL fetching AND the actual SOAP call. The entry is "send:" which dispatches to either "send_VSE" or "send_NonVSE" (for ST/X). There, different mechanisms are used. "send_VSE" calls out to the WinInet DLL, after setting up the parameters, whereas "send_NonVSE" uses the Smalltalk socket interface and/or HTTP framework from Smalltalk/X. In ST/X, the call is asynchronous and non-blocking, meaning that the GUI is still responding while waiting for a response. In VSE, the DLL-call is blocking the VM. Be careful in case of bad/wrong network setup, if a service is unreachable, the GUI may be blocked for a longer time (it is probably a good idea to make the timeouts configurable...)
the basic call entry. By placing a halt on the "value" method, you can single step through the send/receive process. In case of decoding errors (wrong XML/WSDL), it provides the raw XML response via additional getter methods (try #inXmlOrEmpty). Also, in case of xsd:schema errors (wrong restrictions etc.), less strict API entries (extractResuming) are provided which may be able to decode a response even if erroneous.
the new builder class, which was not part of the original SOAP package. Generates the client class, enumerates all reachable non-simple type classes and uses the XeClassBuilder to generate type classes. XeClassBuilder used to be present in the original (2010 SOAP) version, but has been much enhanced to generate reasonable argument and type names, comments and verification code. Also, array handling and restricted simple type handling was added.
In ST/X, the "UserPreferences current" holds all settings of the current user session. In VSE, a mimicri interface is provided for compatibility. This provides some debugging/logging flags of interest:
to enable/disable logging of HTTP traffic on the Transcript. Set to true during development, false for deployed applications. The SOAP__SptHTTPRequest also contains a private Verbose class variable, which can be also set to true (nil counts as false).
controls how much debugging info is emitted from SOAP itself.
if this returns true, you will get a debugger whenever a SOAP-internal error occurs. Otherwise, the error might be reported as a SOAPFault.