12.            Chapter 12 Other SOAP Toolkits

“Just because I don’t care doesn’t mean I don’t understand” – Homer Simpson

 

So far this book has focused on showing you how to build and invoke Web services with VB .NET and VB 6. Being platform-independent, Web services are all about interoperability (interop for short). In fact, the true value of Web services is enabling easy integration of heterogeneous applications. Therefore real-world Web services will likely have a variety of clients written in various languages. This chapter has two objectives that will prepare you for real-world Web services:

·         Familiarize you with other SOAP toolkits that you might encounter whether in client or service implementations

·         Point out some potential interop problems and suggest workarounds

It’s not possible to cover every SOAP implementation out there (there 81 by some counts), so I’ve picked three diverse, popular implementations with broad platform coverage. These are: PocketSOAP for the PocketPC, Apache SOAP for Java, and DHTML Web Service Behavior for Internet Explorer clients.

 

Defining Interop

Is Web Services interop possible? Absolutely. Is it easy? It depends on how you define interop. The term interop is sufficiently vague that it warrants a definition. I define interop as:

A client and service written in different languages successfully communicating data.

 

If you think this definition is too simple, maybe I don’t expect as much as you do. My view is that this definition is the minimum acceptable level of interop, anything beyond that is nice to have but not required. But there are other views on the meaning of interop.

Most developers expect interop between their Web service development tools. As a VB developer, you might expect that VS .NET should be able to add a Web reference to a Java service and auto-generate the right proxy code. You just want to call this proxy and not have to worry about what’s happening under the covers. If VS .NET fails to do this for a specific service, you might think that VB .NET cannot interoperate with that service. But it’s really the tools that are failing here rather than the underlying Web service technologies. Maybe you can edit the Java service’s WSDL and fix it so that VS .NET can read it. Or you can read the WSDL and write the proxy code yourself. The point is, if you understand the root of the problem you can solve it and make interop happen.

I cannot stress this enough: If you want to master Web services development, especially interop, you must clearly distinguish between Web service development tools (e.g. VS .NET) and Web service technologies (e.g. SOAP and WSDL). Otherwise you’ll be bound by the capabilities of the tools you use which are often lagging those of the underlying technologies.

So why are there interop problems? After all, don’t we have specifications that define everything needed to interoperate? Not exactly. In their effort to be simple, and sometimes by mistake, specifications leave out important implementation details or make contradicting recommendations. The result is possible and even likely interop problems between different implementations of the same specification. Here’s an overview of some interop problems you might encounter.

 

Interop Problems

In his MSDN article[1], Keith Ballinger outlined common Web services interop problems broken down by the layer at which they occur. In this section I will describe various interop issues I personally encountered while building Web service applications. Following Ballinger’s lead, I’ll categorize these issues by the layer at which they occur.

HTTP

Although HTTP is a relatively simple protocol, there are specific requirements for using HTTP to invoke a Web service. Some service tools are more stringent about those requirements than others and some client tools are better at meeting those requirements than others.

For example, an HTTP SOAP request should contain a Content-Type header with the character set used:

 

Content-Type: text/xml; charset=utf-8

 

Some clients do not send the character set as part of Content-Type and many Web service implementations don’t know how to handle such requests.

 

Another required HTTP header is the SOAPAction which is a quote-enclosed string e.g. “urn:myservice-action”. Most Web service implementations require that incoming requests contain the appropriate SOAPAction header for the requested operation (some implementations let you turn this off e.g. .NET lets you do this with the RoutingStyle attribute which eliminates dependency on the value of SOAPAction although the SOAPAction header itself is still required as explained in Chapter 6). A client that doesn’t send this SOAPAction header or sends an incorrect value is likely to fail in invoking the required operation. Interestingly, some Web service tools do not take this into account when auto-generating a client proxy and you have to manually edit the generated proxy to send the correct SOAPAction. We’ll see an example of this later in this chapter.

Finally, some services keep track of client sessions through the use of HTTP cookies. This works the same way a Web site with sessions does: The service sends an HTTP cookie to the client and the client is expected to send this cookie back with subsequent requests to the service. If a particular service implementation relies on HTTP cookies, there might be interop issues with clients that do not support this feature.

WSDL

WSDL is a fairly complex specification with many possible scenarios and combinations which leads to high potential for interop problems. For example, some client toolkits do not support non-SOAP bindings in WSDL and fail to read WSDL documents with such bindings. This is a problem particularly for .NET Web services which, by default, have HTTP GET and POST bindings in their WSDL.

Some service toolkits generate WSDL that is simply incorrect with the most common problem being lack of qualified names in references to WSDL items such as binding and operation names.

Some service tools generate WSDL documents broken out into interface and implementation with <import> to import the interface part into the implementation part. Meanwhile some client tools don’t support the WSDL <import> element making them unable to read such WSDL documents.

What does all this mean? It depends on the tool you’re using. As mentioned in Chapter 4, some Web service client tools use WSDL at design time to generate proxy code and others use WSDL at runtime to dynamically generate a proxy. For example, .NET uses WSDL to generate a proxy for you. If for some reason the WSDL is bad (e.g. missing namespace prefixes), you can write the proxy code yourself and proceed to call the service. In this case, the WSDL being incorrect is a design-time issue and has no effect on runtime operation. If however you are using the SOAP Toolkit’s high-level API you need a correct WSDL document at runtime. In this scenario, the WSDL document is an issue at runtime rather than design time. To fix this you can either make a local copy of the WSDL and fix it up or you can switch to using the low level API which does not use a WSDL document at runtime.

 

SOAP

The SOAP message itself is the source of most interop problems and is the focus of current interop efforts. As simple as SOAP is, it specifies four different message formats and an optional encoding style. To date, most interop efforts have focused on RPC/section 5 encoded messages. Within this message format, most interop issues center on section 5 encoding and that’s where most of the interop efforts have focused.

However document/literal messages are easier to interoperate because, by definition, the contents of the SOAP Body are application-specific so there are fewer things that tools have to agree upon. Many popular SOAP tools today support document/literal messages in one way or another (e.g. a low level API) so you should not run into too many problems at the SOAP level when using document/literal messages. However, you typically have to do a lot more work to use document/literal compared to RPC/encoded. But doing more work is not an issue if document/literal becomes your only choice for interoperability in a specific scenario.

In addition to section 5 encoding, one of the SOAP-related interop issues is support for SOAP headers: Some SOAP implementations simply do not support SOAP headers and others do not support header attributes such as mustUnderstand and actor. Services that rely on these SOAP features should ensure that target client tools provide necessary support.

 

The good news is that development tools are getting better at interop and many of them already interoperate quite well. In addition, many tools provide a low-level API where you take full control over request/response messages to do whatever is necessary for interop. But it wasn’t easy getting were we are today, it took much effort from SOAP toolkit builders to enable the level of interop we have today. And things are getting better with each new toolkit release.

 

Interop Efforts

Over the past few years there have been numerous Web services interop efforts. The most successful organized effort was started by Tony Hong of xmethods.net who created an online forum for builders of SOAP toolkits to test interop and communicate and resolve issues. This forum is known as the SOAPBuilders email list at http://groups.yahoo.com/group/soapbuilders/[2]. The actual interop testing is being done in rounds with each round covering progressively more sophisticated features and specifications than the preceding round. Just for your information, here’s how interop testing works:

1.      A list of Web service operations is specified. The operations are chosen to satisfy certain interop scenarios or requirements. For example, echoString is a simple operation that receives a string and echoes it back. This simple operation would uncover basic interop issues, i.e. whether the client and service can communicate at all. echoStringArray is a similar operation that echoes a string array which would uncover basic array interop issues. This list of operations along with some additional information (e.g. SOAPAction, encoding style etc.) becomes the proposed set of interop tests. For example, round 1 proposal is at http://www.xmethods.net/soapbuilders/proposal.html and round 2-A proposal is at http://www.whitemesa.com/interop/proposal2.html.

2.      A WSDL document is created to describe the tests. This WSDL document acts as a test specification, however, the tools being tested may not be WSDL aware. Both round 1 and 2 tests focus on SOAP interop not WSDL.

3.      Builders of SOAP implementations create Web services (using their SOAP implementations) that implement the test specifications. For example, the SOAP Toolkit’s implementation of round 2 tests is at http://mssoapinterop.org/stkV3/Interop.wsdl.

4.      Builders of SOAP implementations create clients (using their SOAP implementations) that invoke other Web services implementing the test suite. Invocation results (success or failure and error message etc.) are usually documented on the Web. For example, test results for .NET Web service client are at http://mssoapinterop.org/results/resaspx.xml

5.      The client and service builders work together on the SOAPBuilders list to resolve any interop issues.

Once a test specification is agreed upon, steps 3-5 are ongoing as SOAP tool builders update their tools and fix interop issues. When the tools achieve sufficient interop success within a test spec (as determined by members of SOAPBuilders), the group moves on to another round of interop testing.

Thanks to the efforts outlined above, we now have a decent level of interop among diverse tools. Let’s examine some of the more popular tools and see examples of interop in action.

PocketSOAP Clients

I’ve attended several talks and read numerous articles that claim they show you how to “develop applications for mobile devices”. In fact, they show you how to build Web applications accessible to mobile devices. That’s not the same thing. A mobile application resides and executes on the mobile device and leverages the capabilities of that device. Many such applications will also need to retrieve or send information from/to the Web. This is an area where Web services really shine: You expose your existing or new applications via Web services and you program mobile clients to access those Web services. That way, your back-end applications are device independent: Any device can communicate with those Web services given a SOAP stack for that device.

PocketSOAP is the de-facto standard SOAP implementation for the PocketPC offering pretty good SOAP support that interoperates with most popular SOAP implementations including the SOAP toolkit and .NET Web services. PocketSOAP exposes a COM-based API that makes it easy to program in Embedded Visual Basic or Embedded Visual C++. In addition, PocketSOAP also comes in a Win32 implementation that you can use from any COM-aware language (including script languages) on Win32 platforms (even Windows 95). Both the Win32 and PocketPC implementations expose nearly identical object models making it easy to learn and program PocketSOAP using VB 6 then moving your skills and code to Embedded VB.

Figure 12-1 shows the essential PocketSOAP classes. I left out classes and interfaces that have to do with custom serialization. I will not explain how to do custom serialization with PocketSOAP but know this: PocketSOAP supports a serialization model very similar to the SOAP Toolkit. You can create a custom serializer to serialize application-specific objects by implementing ISoapSerializer.

 

 

Figure 12‑1

The essential PocketSOAP classes

 

To send a SOAP request, you begin by creating a CoEnvelope object. CoEnvelope exposes properties that let you set various parts of the physical SOAP Envelope including MethodName (name of the Web service operation) and URI (namespace of the Web service operation). CoEnvelope also exposes Web service operation parameters as a collection of CoSoapNode objects. Each CoSoapNode represents an XML element inside the message’s Body. Using CoSoapNode you can set/read all aspects of the corresponding XML element including the element’s name, namespace, text content (value).

Instead of using custom serializers, you can create new CoSoapNode objects and set their name, namespace, and value yourself. This is very similar to using the SOAP Toolkit’s low level API.

HTTPTransport is responsible for sending the HTTP request and receiving the response. In addition to HTTP, PocketSOAP also supports custom transports as COM classes that implement ISOAPTransport. For example, a downloadable TransportPak provides support for several other transports including raw TCP.

RPC/encoded Messages with PocketSOAP

To demonstrate interoperability, I built a client (shown in listing 12-1) that invokes the VB .NET Weather service using PocketSOAP 1.2.

 

Listing 12‑1 A PocketSOAP client invoking a .NET Weather service. (VBWSClientCode\PocketSoapClient\frmWeather.frm).

Private Sub cmdGet_Click()

    Dim envReq As PocketSOAP.CoEnvelope

   

    Set envReq = New PocketSOAP.CoEnvelope

    envReq.MethodName = "GetWeather"

    envReq.URI = "http://tempuri.org/"

    envReq.Parameters.Create "zipCode", txtZip.Text

    Dim http As PocketSOAP.HTTPTransport

    Set http = New PocketSOAP.HTTPTransport

   

    'change this to point to your proxy if you have one

    'http.SetProxy "pluto", 8080

    http.SOAPAction = "http://tempuri.org/GetWeather"

    Call http.Send( _

             "http://www.learnxmlws.com/services/WeatherRetriever.asmx", _

             envReq.Serialize)

    Dim envResp As PocketSOAP.CoEnvelope

    Set envResp = New PocketSOAP.CoEnvelope

    Call envResp.Parse(http)

    Call DisplayData(envResp)

   

End Sub

Private Sub DisplayData(response As PocketSOAP.CoEnvelope)

    Dim result As CoSoapNode

    Set result = response.Body.Item(1)

    lblConditions.Caption = result.Nodes.ItemByName("Conditions").Value

    lblCurrentTemp.Caption = result.Nodes.ItemByName("CurrentTemp").Value

    lblInfo.Caption = "Barometer at " & _

    result.Nodes.ItemByName("Barometer").Value & " and " & _

    result.Nodes.ItemByName("BarometerDirection").Value & vbCrLf & _

    "Humidity is " & result.Nodes.ItemByName("Humidity").Value * 100 & "%"

   

End Sub

To invoke the GetWeather operation, I create a CoEnvelope object and set its MethodName to GetWeather and its URI to http://tempuri.org/. To send the zip code, I need to create a new CoSoapNode and set its name to zipCode and its value to the user-entered zip code. I do this by accessing the envelope’s parameters collection and calling its Create method. This method creates the CoSoapNode and adds it to the collection in one step.

To send the message, I create an HTTPTransport object and set its SOAPAction property then call its Send method passing it the service’s end point URL and the serialized envelope. Note that calling envReq.Serialize serializes the SOAP envelope along with any parameters you’ve added to it and returns the serialized SOAP message as a string.

To retrieve the response, I create a new CoEnvelope, named envResp, and call its Parse method passing it the HTTPTransport object. This new envelope now contains the response message. DisplayData handles reading out information from the response message using the result.Nodes collection which contains CoSoapNode objects. As a point of reference, listing 12-2 shows a template response SOAP Body.

 

Listing 12‑2 A template response message from the Weather service.

<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

  <tns:GetWeatherResponse>

    <GetWeatherResult href="#id1" />

  </tns:GetWeatherResponse>

  <types:CurrentWeather id="id1" xsi:type="types:CurrentWeather">

    <LastUpdated xsi:type="xsd:string">string</LastUpdated>

    <IconUrl xsi:type="xsd:string">string</IconUrl>

    <Conditions xsi:type="xsd:string">string</Conditions>

    <CurrentTemp xsi:type="xsd:float">float</CurrentTemp>

    <Humidity xsi:type="xsd:float">float</Humidity>

    <Barometer xsi:type="xsd:float">float</Barometer>

    <BarometerDirection xsi:type="xsd:string">string</BarometerDirection>

  </types:CurrentWeather>

</soap:Body>

DisplayData first declares a variable named result and sets it to the item corresponding to the GetWeatherResult element. Using this result variable, I can then get its contents using its Nodes property which returns a collection of CoSoapNode objects. This collection contains a CoSoapNode object for each element in the CurrentWeather structure shown in listing 12-2. For example, to get the text content of the Conditions element, I call result.Nodes.ItemByName(“Conditions”).Value. By using CoSoapNode and CoSoapNodes, I do not need to write a custom deserializer for the CurrentWeather structure. Figure 12-2 shows the PocketSOAPClient in action.

 

Figure 12‑2

The PocketSOAP client displaying current weather information.

 

Document/literal Messages with PocketSOAP

In addition to RPC/encoded messages, PocketSOAP also supports document/literal messages. To show you an example of this, I built a PocketSOAP client that invokes the GetCustomersDataSet operation explained in Chapter 9. Running this sample invokes the operation and displays the returned data in a listview as shown in figure 12-3. Listing 12-3 shows the sample client code.

 

 

Figure 12‑3

Using PocketSOAP to invoke a document/literal service and displaying the results.

 

Listing 12‑3 PocketSOAP client code for invoking a document/literal Web service. (VBWSClientCode\PocketSoapClient\frmDocument.frm).

Private Sub cmdGetCustomers_Click()

    Dim envReq As PocketSOAP.CoEnvelope

    Set envReq = New PocketSOAP.CoEnvelope

    envReq.EncodingStyle = ""

        Dim node As CoSoapNode

    Set node = New CoSoapNode

    node.Name = "GetCustomersDataSet"

    node.Namespace = "http://www.LearnXmlWS.com/customerorders"

    Call envReq.Body.Append(node)

    Dim http As PocketSOAP.HTTPTransport

    Set http = New PocketSOAP.HTTPTransport

   

    'change this to point to your proxy if you have one

    'http.SetProxy "pluto", 8080

    http.SOAPAction = _

         "http://www.LearnXmlWS.com/customerorders/GetCustomersDataSet"

    Call http.Send( _

         "http://VBWSServer/vbwsbook/Chapter9/CustomerOrders.asmx", _

         envReq.Serialize)

    Dim envResp As PocketSOAP.CoEnvelope

    Set envResp = New PocketSOAP.CoEnvelope

    Call envResp.Parse(http)

    Call ShowCustomers(envResp)

   

End Sub

Private Sub ShowCustomers(response As CoEnvelope)

lvw.ListItems.Clear

    Dim li As ListItem

    Dim result As CoSoapNode

    Set result = response.Body.ItemByName("GetCustomersDataSetResponse") _

                    .Nodes.ItemByName("GetCustomersDataSetResult")

    Dim theData As CoSoapNode

    Set theData = result.Nodes.ItemByName("diffgram") _

                    .Nodes.ItemByName("NewDataSet")

    Dim aCustomer As CoSoapNode

    For Each aCustomer In theData.Nodes

        Set li = lvw.ListItems.Add(, , _

              aCustomer.Nodes.ItemByName("CustomerID").Value)

        Call li.ListSubItems.Add(, , _

              aCustomer.Nodes.ItemByName("CompanyName").Value)

    Next

End Sub

To send a document/literal message, I create a CoEnvelope and set its EncodingStyle property to an empty string. The default encoding style is section 5 encoding which is used with RPC. By definition, there is no encoding for literal messages hence the empty string.

I then create a new CoSoapNode which represents the literal contents of the request’s Body. In this case, all I need to send is an element called GetCustomersDataSet with the namespace URI "http://www.LearnXmlWS.com/customerorders". I then append this node to the Body before sending the message with an HTTPTransport object.

A procedure named ShowCustomers (in listing 12-3) is responsible for pulling data out of the response and filling the listview. To do this it first extracts the GetCustomersDataSetResult element then uses it to extract the NewDataSet element (see Chapter 9 for a description of the response document). It then enumerates each child element within NewDataSet and extracts CustomerID and CompanyName information from it and displays this information in the listview.

The code in listing 12-3 reminds me of using the SOAP Toolkit’s low level API: It seems like a lot of code but is actually fewer lines than you would have to write and maintain if you decided to create custom serializers. It is also a consistent programming model that you have to learn once then use for any Web service you want to invoke.

Java Clients

Owing to the relatively recent popularity of Java and J2EE, many businesses have Java applications that need to be integrated with VB/COM applications. For this reason, I consider interop with Java clients/services an important element of successful Web service implementations.

There are many SOAP implementations in the Java world ranging from free, open source implementations to productized, fully supported tools. Some of the tools participating in interop testing include Apache SOAP, Apache Axis, WASP Developer (from Systinet) and GLUE (from The Mind Electric).

Apache SOAP is one of the original, early-on SOAP implementations. It started as an IBM SOAP stack named SOAP4J and was then turned over to the Apache Software Foundation (www.apache.org) and became an open-source project.

In addition to Apache SOAP, there’s a newer open-source implementation named Apache Axis. Axis is considered a follow on project to Apache SOAP with a complete re-architecture aiming at tighter WSDL integration and better EJB support. However, at the time of this writing, Apache Axis is still in the beta stages and there are far more Web services and clients using Apache SOAP than Apache Axis.

IBM’s Web Services Toolkit

Apache SOAP is just a SOAP implementation. It lacks some of the additional tools typically needed for Web service development most notably a WSDL generation and consumption tool. IBM provides a Web Services Toolkit (WSTK Version 2.4) which comes with Apache SOAP and several other Web service related tools including a WSDL generation wizard, a WSDL-based Java proxy generator, and a UDDI SDK.

You can download the WSTK for free at http://www.alphaworks.ibm.com/tech/webservicestoolkit. To install the SDK, you’ll first need to download and install JDK 1.3 from http://java.sun.com/j2se/1.3/download-windows.html. After you’ve installed the WSTK (a typical install is fine), you’ll need to perform various configuration steps depending on which parts of the WSTK you’ll need to use. To run the examples in this section, you’ll need to perform only four configuration steps (none of the other WSTK configuration is required):

·         Make sure the PATH environment variable includes the JDK’s bin folder. For example, if you installed the JDK to E:\JDK1.3 make sure you add E:\JDK1.3\bin to the PATH environment variable. You can do this by opening the properties for My Computer, clicking on the Advanced tab, then clicking on Environment Variables. Look for the Path variable (either under your list of environment variables or the System’s list) and double click on it to edit it.

·         Make sure the JAVA_HOME environment variable is defined and points to the folder where you installed the JDK. For example, if you installed the JDK in E:\JDK1.3 make sure JAVA_HOME is defined and points to E:\JDK1.3. You can do this in the same way you configured the PATH variable above.

·         SET the WSTK_HOME environment variable to point to the location where you installed the WSTK. For example, if you installed the WSTK in D:\WSTK, the set WSTK_HOME to D:\WSTK. Do not add a backslash at the end of the path. You can set this environment variable the same way you set the PATH and JAVA_HOME variables.

·         Each example in the following two sections includes two batch files: build.bat and run.bat. For these batch files to work, you need to define an environment variable named VBWSCLIENTCODE and set it to the path where you copied the book’s client code (without a backslash at the end). For example, if you copied the book’s client code to D:\VBWSClientCode then you need to define the environment variable VBWSCLIENTCODE and set it to D:\VBWSClientCode. You can set this environment variable the same way you set the other environment variables above.

 

The toolkit provides three WSDL tools that handle generating code from WSDL and WSDL from code. The first tool, WSDLGen, plays a role similar to that of the SOAP Toolkit’s WSDL wizard: It generates WSDL from a Java Class file, an EJB jar file, or a COM interface. The generated WSDL is interesting in that it is separated in two files: One file contains the interface elements, namely everything except the <service> element. The second file imports the interface file and contains the <service> element (implementation details). This separation can be useful for many things including registering the service interface with UDDI (see Chapter 11) and handing it to other developers who will build services that implement this interface.

The other two tools, proxygen and servicegen play the role of .NET’s wsdl.exe. Proxygen generates client-side proxies from WSDL documents and servicegen generates service skeleton implementations or templates from WSDL documents.

Each of these tools has a corresponding batch file that you use to run the tool. These batch files are all located in the bin folder under the folder where you installed the WSTK. To run a tool, simply run the batch file with the same name.

In the text two sections I’ll show you two examples of using the WSTK to invoke an RPC/encoded and a document/literal Web service respectively.

RPC/encoded Messages

As an example of RPC/encoded interop, we’ll call our trusty Weather service on learnxmlws.com. To generate a Java proxy class you need to run proxygen.bat on the service’s WSDL document. Proxygen does not currently support HTTP GET and HTTP POST bindings which are in the weather service’s WSDL document. But don’t worry, proxygen will simply ignore bindings it doesn’t understand so you should only get a couple warnings but no real errors. When you are ready to generate the client-side proxy, type the following command:

 

%WSTK_HOME%\bin\proxygen -outputdir TheOutputLocation http://www.learnxmlws.com/services/weatherretriever.asmx?wsdl

 

Where TheOutputLocation is where you want output files to go. Proxygen will create a directory structure based on the service’s namespace. For example, the weather service’s namespace is http://tempuri.org/ so the resulting directory structure is org\tempuri. Within the tempuri directory proxygen creates a file named WeatherRetrieverSoap.java which is the Web service proxy class that clients use to invoke the service. It also creates a CurrentWeather.java file that contains the CurrentWeather class. This class corresponds to the CurrentWeather complex type returned from the service’s GetWeather operation. To deserialize the returned XML into an instance of CurrentWeather, proxygen creates a (de)serializer class in a file named CurrentWeatherSerializer.java. Proxygen also compiles all generated .java files resulting in .class files with the same names.

I ran into one problem with the generated proxy: It does not specify the correct SOAPAction for each Web service operation. Instead the generated WeatherRetrieverSoap class has a private member variable named SOAPActionURI which is set to "http://tempuri.org/GetWeather" in the class’s constructor. This is easy enough to fix, simply open WeatherRetrieverSoap.java and add the following line to GetTemperature function:

 

this.SOAPActionURI = "http://tempuri.org/GetTemperature";

 

Also add this line to the GetWeather function:

this.SOAPActionURI = "http://tempuri.org/GetWeather";

 

Note that you don’t need the extra double quotes around the SOAPAction value, those are added by Apache SOAP. The book’s example code includes an already-edited WeatherRetrieverSoap.java file. In addition to the above two lines, the supplied file also includes a SetProxy function that you can use to set an HTTP proxy (for example if you want to use ProxyTrace with these examples) by uncommenting two lines of code.

Now you are ready to write client code that uses the generated proxy to invoke the weather service. Listing 12-4 shows an example client that invokes GetTemperature.

 

Listing 12‑4 A Java client using Apache SOAP to invoke a .NET Web service (RPC/encoded messages). (VBWSClientCode\Chapter12\java\org\tempuri\TempClient.java).

package org.tempuri;

 

public class TempClient

{

public static void main(String[] args) throws Exception {

         

  WeatherRetrieverSoap wr=

              new WeatherRetrieverSoap(

                 new java.net.URL(

          "http://www.learnxmlws.com/services/weatherretriever.asmx"));

  float t= wr.GetTemperature(args[0]);

  System.out.println("\r\nTemperature for zipCode " +

                           args[0] + "...\r\n");

  System.out.println(t);

}

The first line tells the compiler that this class belongs to the org.tempuri package. Think of a Java package as the equivalent of a .NET namespace. The generated proxy class, WeatherRetrieverSoap, belongs to org.tempuri so I put this client class in the same package. Although this is not necessary, it makes it sightly easier to build and run the client.

The client class is named TempClient (temperature client), it has one static method named main which is the class’s entry point. Inside this method, I first create a new WeatherRetrieverSoap object passing its constructor a URL object initialized with the service’s URL. I then call the proxy’s GetTemperature function passing it the first argument that the user passed to the class. Then I print out the returned temperature using System.out.println.

To build this example, open a command prompt and change the current drive to the drive where you copied the books code, e.g. D:, then run %VBWSCLIENTCODE%\Chapter12\Java\org\tempuri\build.bat. After running build.bat, run runTemp.bat passing it a zip code, for example:

%VBWSCLIENTCODE%\Chapter12\Java\org\tempuri\runTemp 20171

The resulting output should look like (hopefully it’ll be warmer where you live):

 

Temperature for zipCode 20171...

 

36.0

 

The preceding example used only simple types: It took in a string and returned a float. Let’s take a look at an example that uses complex types. Listing 12-5 shows an example client that invokes GetWeather.

Listing 12‑5 Invoking GetWeather and handling the complex return type from Java. (VBWSClientCode\Chapter12\java\org\tempuri\GetWeather.java).

package org.tempuri;

public class WeatherClient

{

public static void main(String[] args) throws Exception {

  WeatherRetrieverSoap wr=

          new WeatherRetrieverSoap(new java.net.URL(

         "http://www.learnxmlws.com/services/weatherretriever.asmx"));

  org.tempuri.CurrentWeather cw= wr.GetWeather(args[0]);

  System.out.println("\r\nTemperature for zipCode " + args[0] +

                             "...");

  System.out.println(cw.CurrentTemp);

  System.out.println("\r\nCurrent conditions for zipCode " + args[0] +

                            "...");

  System.out.println(cw.Conditions);

 

}

}

This client is very similar to the Temperature one except it calls GetWeather instead of GetTemperature. GetWeather returns an instance of the CurrentWeather class that was generated by proxygen, so this client captures the returned object in a variable named cw then reads data out of the object’s public fields and displays it using System.out.println. The heavy lifting (as far as deserializing the returned XML into a CurrentWeather object) is done by the generated CurrentWeatherSerializer class.

To run this client, first build it by running build.bat then run Weather.bat passing it the zip code:

 

%vbwsclientcode%\chapter12\java\org\tempuri\runWeather.bat 20171

 

Temperature for zipCode 20171...

36.0

 

Current conditions for zipCode 20171...

clear

 

As you can see, using the WSTK for Java clients is fairly straightforward as long as your service uses RPC/encoded and if you rememer to set the appropriate SOAPAction in the generated proxy class.

You can make it easier for developers using the WSTK to call your Web services by not relying on SOAPAction. To do this, set the RoutingStyle property of SoapRpcService to RequestElement. See Chapter 6 for more information.

 

Document/literal Messages

Apache SOAP also supports document/literal message, although proxygen cannot handle WSDL documents with document/literal messages. Instead, you have to write the client proxy yourself using the Apache SOAP object model. This is similar to using the SOAP Toolkit’s low level API.

To demonstrate document/literal interop I created a Java client that invokes the CustomerOrders Web service from Chapter 9. The client first calls GetCustomerOrdersTypedDataSet to retrieve the customer order data. Using XML DOM, it edits this DataSet changing the first customer’s company name to the user-specified name. It then invokes SaveCustomerOrdersTypedDataSet sending it the modified data to be saved. This sample’s code is divided among three classes.

CustomerData is the Web service proxy class. It exposes three public methods, GetCustomerOrdersTypedDataSet, SaveCustomerOrdersTypedDataSet and GetCustomersXml that invoke the corresponding Web service operations. We won’t use GetCustomersXml in this example but I implemented it as a reference for you for invoking an operation that returns an XML document.

To manipulate the DataSet in Java, CustomerData relies on a class called JDataSet. You might think JDataSet is an implementation of the ADO.NET DataSet for Java. That would be great, unfortunately however, JDataSet is a simple class I created to manipulate the CustomerOrders typed dataset and create a diffgram (a dataset copy with the changes) to be sent back to the service. JDataSet does this manipulation using the XML DOM.

The third class, CustomerClient, is the Java client that uses CustomerData and CustomerData.JDataSet.  It is a rather simple console application that takes in the new company name, invokes GetCustomerOrdersTypedDataSet, updates the first company name to the new name you supplied, then calls SaveCustomerOrdersTypedDataSet with the updated JDataSet. Listing 12-6 shows the relevant part of the CustomerData proxy class which is where the Apache SOAP action is.

 

Listing 12‑6 A Java proxy class for a .NET document/literal service. (VBWSClientCode\Chapter12\java\document\CustomerData.java).

public class CustomerData

{

 

private URL _endPointUrl;

private static final String GetCustomerXmlAction=

     "http://www.LearnXmlWS.com/customerorders/GetCustomersXml";

private static final String GetCustomersDSAction=

     "http://www.LearnXmlWS.com/customerorders/GetCustomerOrdersTypedDataSet";

private static final String

SaveCustomersDSAction=

    "http://www.LearnXmlWS.com/customerorders/SaveCustomerOrdersTypedDataSet";

public CustomerData(URL endPointURL)

{

      this._endPointUrl= endPointURL;

}

public JDataSet GetCustomerOrdersTypedDataSet() throws Exception {

Envelope env=new Envelope();

Document doc= new DocumentImpl();

      Element bodyEntry= doc.createElementNS(

       "http://www.LearnXmlWS.com/customerorders",

       "ns1:GetCustomerOrdersTypedDataSet");

Vector inDoc=new Vector(1);

inDoc.addElement(bodyEntry);

Body body=new Body();

body.setBodyEntries(inDoc);

      env.setBody(body);

      Message msg=new Message();

this.setProxy(msg);

msg.send(_endPointUrl,GetCustomersDSAction,env);

      JDataSet jds=new JDataSet(msg);

      return jds;

 

}

public void SaveCustomerOrdersTypedDataSet(JDataSet jds) throws Exception {   

Envelope env=new Envelope();

Document doc= new DocumentImpl();

Element bodyEntry= doc.createElementNS(

    "http://www.LearnXmlWS.com/customerorders",

    "ns1:SaveCustomerOrdersTypedDataSet");

bodyEntry.setAttribute(

          "xmlns","http://www.LearnXmlWS.com/customerorders");

bodyEntry.appendChild(doc.importNode(jds.diffGram,true));

Vector inDoc=new Vector(1);

inDoc.addElement(bodyEntry);

Body body=new Body();

body.setBodyEntries(inDoc);

env.setBody(body);

Message msg=new Message();

this.setProxy(msg);

msg.send(_endPointUrl,SaveCustomersDSAction,env);

}

The code in listing 12-6 begins by defining a few private members that represent the end point URL and the various SOAP action values. The constructor takes in the service’s end point URL and stores it in the _endPointUrl member variable. GetCustomerOrdersTypedDataSet acts as a wrapper for the Web service’s GetCustomerTypedDataSet except it returns a JDataSet object. To invoke this document/literal service, I need to send a SOAP message like the one in listing 12-7.

 

Listing 12‑7 The document/literal request message.

<soap:Envelope

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">

  <soap:Body>

    <GetCustomerOrdersTypedDataSet

         xmlns="http://www.LearnXmlWS.com/customerorders" />

  </soap:Body>

</soap:Envelope>

To do this, I first create a new Envelope object which represents the message’s Envelope. Then I create a new XML document object which I use to create an XML Element named GetCustomerOrdersTypedDataSet. Note that you can’t create an XML element directly, you must first create an XML document then use its createElementNS method to create an element in a namespace. To add this element to the message Body, I first create a Vector object and add this XML element to this Vector. I then create a new Body object which represents the message Body and attach the entire Vector object to it by calling body.setBodyEntries. Next, I attach this Body to the Envelope by calling Envelope.setBody.

To send the message, I create a new Message object named msg and call this.setProxy. setProxy is a private function that tells Apache SOAP to use an HTTP proxy. If you want to use an HTTP proxy (e.g. ProxTrace), edit setProxy and uncomment the two commented lines. Next, I call msg.send passing it the Web service’s URL, the SOAPAction value, and the envelope I created. To parse out the returned XML I create a new JDataSet object passing msg to its constructor. JDataSet’s constructor extracts the returned XML from the SOAP response message and stores it in a member variable named ds which is actually an XML Element. I then return this JDataSet to the caller.

SaveCustomerOrdersTypedDataSet is equally straightforward. It creates the request message in much the same way as GetCustomerOrdersTypedDataSet. The main difference being the way it appends the diffGram to the request message by calling bodyEntry.appendChild.

This is basically using standard DOM methods to prepare the request document. JDataSet.diffGram returns an XML DOM Element that contains the diffGram (the updated DataSet) to be sent back to the service. The call to appendChild appends this diffGram element with all its contents into the request element thereby forming the literal document that will be sent to the Web service. This literal document is then appended to the body the same way as in the previous listing and the message is sent off to the service with the appropriate SOAPAction.

Listing 12-8 shows the client using this proxy to retrieve, update, and save the DataSet.

Listing 12‑8 A Java client using the proxy class in listing 12-6. (VBWSClientCode\Chapter12\java\document\CustomerClient.java).

public class CustomerClient

{

        private static final String SERVICE_URL=

           "http://localhost/vbwsbook/Chapter9/CustomerOrders.asmx";

public static void main(String[] args) throws Exception {

           if(args.length ==1) {

 

    CustomerData cd=new CustomerData(new java.net.URL(SERVICE_URL));

    System.out.println("retrieving customers DataSet ...");

    JDataSet jds=cd.GetCustomerOrdersTypedDataSet();

    System.out.println("updating company name ...");

    jds.UpdateCompanyName(0,args[0]);

    cd.SaveCustomerOrdersTypedDataSet(jds);

    System.out.println("Updated company name");

   }

  else

    System.out.println("Please specify a new company name");

}

}

The client first defines a private static member to hold the service end point URL (you’ll want to modify this URL to point to your service’s location). It then creates a new CustomerData object and calls GetCustomerOrdersTypedDataSet capturing the returned JDataSet in a variable named jds. It then calls UpdateCompanyName passing it the record number to update (0 is the first record) and the user-specified new company name. UpdateCompanyName uses XML DOM to find the specified record then update it and create a new diffGram XML element and stores it in a member variable named diffGram. The code in UpdateCompanyName does not use any Apache SOAP features: It is strictly XML DOM code. So if you’ve programmed XML DOM (e.g. using MSXML and/or System.Xml) this code will be easy to understand.

At this point, the data has been retrieved and updated. To save this data, we need to send it to the Web service. The next line calls SaveCustomerOrdersTypedDataSet passing it the JDataSet object.

To run this client first run build.bat to build the Java classes. Then run run.bat with a new company name. Be sure to use quotes around the name if it contains spaces. For example:

 

run.bat "this is a new name"

 

retrieving customers DataSet ...

updating company name ...

Updated company name

 

This example shows that invoking document/literal Web services with Apache SOAP is a straightforward process that corresponds logically to the SOAP request/response message. Most of the code will be in forming/manipulating the request/response XML documents which is usually the case with document/literal messages. The good news is: This code can use standard APIs such as SAX or DOM so whoever is developing the client (you or someone else) will probably already have the necessary skills to create these XML documents.

The Web Service Behavior

There’s no doubt that browsers are now the standard application client for both intranet and Internet applications. Given that Internet Explorer is the most popular browser, you might want to leverage IE features when building intranet applications. One of IE’s interesting features is the Web service DHTML behavior which lets you invoke Web services right from client-side script code. Being a DHTML behavior, there’s nothing to install on the client, except of course IE 5.0 or later. In this section I will show you how to use the Web service behavior to invoke RPC/encoded and document/literal services.

 

Setting Up the Behavior

To get started with the Web service behavior, you first need to copy Webservice.htc to a location on your Web server. This file is on the book’s CD (in the extras folder) and you can also get it from http://msdn.microsoft.com/workshop/author/webservice/webservice.htc. To use the behavior on a page, you must attach it using the BEHAVIOR CSS style, for example:

<DIV id=ws  

 style="BEHAVIOR: url(webservice.htc)"

    onresult=onWeatherResult();>

</DIV>

When using the behavior, the default programming model is asynchronous: You invoke a Web service asynchronously and the behavior informs you via a callback or event handler when the call returns. The onresult attribute hooks up an event handler to be executed when the service call returns. You are now ready to use the Web service behavior from your DHTML page.

 

Invoking Web Services

To invoke a Web service, you first call the behavior’s useService method passing it the service’s WSDL URL and a friendly name for the service. The friendly name is any string that you will use later to refer to this service. One Web service behavior can handle multiple Web services, so the friendly name you assign for each service needs to be unique.

A good place to call useService is upon page load. You can do this by hooking up a function to the body’s onload event. This function can call the behavior’s userService method to set up all the services you plan to call using this behavior. Listing 12-9 shows an example function called Init that associates a weather service with the friendly name Weather.

 

Listing 12‑9 Calling the Weather .NET service from the DHTML Web service behavior. (VBWSBook\Chapter12\WSBehavior.html).

<script language=JScript>

function Init() {

      ws.useService(

        "http://vbwsserver/vbwsbook/chapter12/weather.asmx?wsdl",

        "Weather");

}

function GetTemp() {

  errInfo.innerHTML="";

  conds.innerHTML="";

  temp.innerText="";

  var objCall=new Object();

  objCall.async=false;

  objCall.funcName="GetTemperature";

  objCall.params=new Array();

  objCall.params.zipCode=txtZipCode.value;

  var objResult= ws.Weather.callService(objCall);

  if(!handleError(objResult))

temp.innerText="temperature is: " + objResult.value;

}

</script>

Note: For security reasons, when using the Web service behavior, IE will let you invoke services that reside on the same Web server where the page was served from. If you want to call a Web service that’s not on your server, you’ll need to build a wrapper Web service that forwards requests to the real Web service. For example, I wrote a wrapper Web service for the Weather service on LearnXmlWS.com. This wrapper service is in the chapter’s Web code folder in the files Weather.asmx and Weather.asmx.vb.

 

The function GetTemp in listing 12-9 is executed when the user clicks on a Get Temperature button. The function first initializes some display elements then creates a new object called objCall. objCall represents the Web service behavior’s call object which contains information about the operation you want to call. For example, funcName is the Web service’s operation name while params is an associative array (similar to a dictionary object) of parameters. In this example, funcName is GetTemperature and the only parameter in params is called zipCode and is set to the user-entered zip code.

You can also use objCall to set the async property to false like I do in this example. This causes the Web service call to be synchronous thereby blocking the user interface thread until the call returns. You normally don’t want to do this unless you are sure the Web service call will return immediately or when you intentially want the UI to be frozen until the call returns.

To invoke the Web service, you call the behavior’s callService function passing it the call object. The way you call callService is interesting. First, you access a property of the behavior with the same name as the Web service’s friendly name that you assigned in useService. This property returns an object which acts as the Web service proxy. In this example, I used the friendly name “Weather” so ws.Weather is the Web service proxy. I then call ws.Weather.callService passing it the call object. This call returns a result object from which you can get the call status (success or failure), any error information, and of course the returned data.

If you are just using the call object to set the operation’s name and parameters, you can pass those directly to the callService method. However, the call object lets you specify options such as async.

To check for errors, Listing 12-9 calls a function named handleError which returns  true if there was an error. If there wasn’t an error, the temperature is retrieved using objResult.value then displayed on the page.

The handleError function is shown in listing 12-10. handleError displays the error information on the page itself. is the code in listing 12-9 displays the returned temperature using the results object’s errorDetail property.

 

Listing 12‑10 Checking for errors when using the DHTML Web service behavior. (VBWSBook\Chapter12\WSBehavior.html).

function handleError(objResult) {

    if((objResult.error)) 

    {   

        alert("There was an error!");

        errInfo.innerHTML="An error occurred: <br>Fault code" +

       objResult.errorDetail.code + "<br>Fault string: " +

       objResult.errorDetail.string;

      

        return true;

    }

    else

    {

     return false;

    }

}

The errorDetail property returns an object roughly equivalent to the SOAP Fault element. errorDetail has three properties: string returns the faultstring, code returns the faultcode. The property named raw returns an XML DOMDocument object that contains the response SOAP envelope. You can use standard DOM properties and methods on this document to read out information from it or even transform it using XSLT.

 

Handling Complex Types

Now that you know the basics of using the Web service behavior, the next step is to call a more sophisticated operation that returns a complex type. Listing 12-11 shows an example that calls the GetWeather operation which returns richer weather information.

Listing 12‑11 Handling complex types with the Web service behavior. (VBWSBook\Chapter12\WSBehavior.html).

function GetWeather() {

  errInfo.innerHTML="";

 

  conds.innerHTML="<b>Retrieving weather information ...</b>";

  var objCall=new Object();

  objCall.async=true;

  objCall.funcName="GetWeather";

  objCall.params=new Array();

  objCall.params.zipCode=txtZipCode.value;

  ws.Weather.callService(objCall);

}

function onWeatherResult() {

if(!handleError(event.result))

{

  var doc=event.result.raw;

  temp.innerText=doc.selectSingleNode("//CurrentTemp").text;

  conds.innerHTML="Current conditions: " + 

          doc.selectSingleNode("//Conditions").text +

    "<br>" + "Humidity: " + doc.selectSingleNode("//Humidity").text;

  icon.src=doc.selectSingleNode("//IconUrl").text; 

}

}

The function named GetWeather uses the same Web service behavior as the function named GetTemperature. After initializing a couple of display elements, GetWeather creates a call object then sets the funcName to GetWeather and adds a parameter named zipCode. This time, the async property is set to true indicating that the Web service call will be asynchronous and the behavior will fire the onresult event when the call returns. This is why GetWeather does not attempt to read the returned weather information.

When onresult is fired, the event handler function is called. In this example, onWeatherResult is the event handler for the onresult event. Within the event handler, you get access to the result object by calling event.result.

First, onWeatherResult checks for errors by calling handleError. Assuming no errors occurred, it then calls event.result.raw to get back the returned SOAP message as a DOMDocument object. This is an easy way to handle complex types: you just read them as XML documents. In this example, I use XPath with selectSingleNode to read out the current temperature, conditions, and humidity from the returned document. I also read the IconUrl and use it as the src for the HTML img tag called icon. The result is displayed as in figure 12-4.

 

Figure 12‑4

A Document/literal Example

Once you know how to handle complex types with the Web service behavior, invoking document/literal services is no different: You access the returned message using result.raw. As an example, I wrote some code to invoke the GetCustomersXml operation from chapter 9. Since the client is browser-based, it’s likely you’ll want to transform the returned XML to HTML for display. To do this, I used an XSLT stylesheet and the MSXML2.DOMDocument object. Listing 12-12 shows the example code.

 

Listing 12‑12 Calling a document/literal Web service and transforming the response using an XSLT stylesheet.

function GetCustomers() {

customerWS.useService(

 "http://VBWSServer/vbwsbook/Chapter9/CustomerOrders.asmx?wsdl","Customers");

  var objCall=new Object();

  objCall.async=true;

  objCall.funcName="GetCustomersXml";

  customers.innerText="calling service ...";

  customerWS.Customers.callService(displayCustomers,objCall);

 

 

}

function displayCustomers(objResult) {

if(!handleError(objResult))

{

    var xsldoc=new ActiveXObject("MSXML2.DOMDocument.3.0");

    xsldoc.async=false;

    xsldoc.load("customers.xsl");

              try {

    customers.innerHTML=objResult.raw.transformNode(xsldoc);

                }

                catch(ex)

                   {

                     alert( "Error transforming XML. " +

                          "Do you have MSXML3 installed?\n" +

                           "Click OK to display the returned XML");

                     customers.innerText=objResult.raw.xml;

                   }

 

}

}

The function GetCustomers uses another Web service behavior named customerWS (you can have many Web service behaviors on the same page). It first initializes the behavior by calling useService and passing it the service’s URL and a service friendly name. It then creates a call object and sets funcName to GetCustomersXml. This time, there is no event handler for the onresult event. Instead, when calling the service, the name of a callback function, displayCustomers in this example, is passed as the first parameter. The advantage of using callbacks rather than an event handler is that you can have a different callback for every Web service operation that you call.

The displayCustomers callback function is also shown in listing 12-12. After checking for errors, it creates a new MSXML2.DOMDocument object named xsldoc. This object is then used to load the customers.xsl stylesheet. Note that xsldoc.async is set to false before loading the document. Otherwise, the document will be loaded asynchronously which means the code would have to first check whether the document has been loaded before actually using it. The returned customer information is accessible as an XML DOMDocument object via the result.raw property. You can transform this XML to HTML by calling transformNode on this DOMDocument and passing it an XSLT stylesheet. transformNode applies the XSLT transformation and returns the result as a string that you can display using the innerHTML property of a UI element as in this example. The result is a list of customer ids and their company names formatted as an HTML table.

 

VB 6 Clients

So far this chapter has focused on invoking VB services from other clients. Starting with this section, we turn things around and look at invoking Web services from VB 6 clients and VB .NET. This section covers VB 6 clients, the next section covers VB .NET clients.

The Microsoft SOAP Toolkit team is an active participant in the SOAP interop efforts outlined earlier in this chapter. As a result, clients using the toolkit can call just about any Web service out there. In some cases, the Web service you want to call will have some quirky aspects that make it difficult or impossible to call with the high-level API. For example, the service might have a bad WSDL document or might not have one at all. In these cases, you can use the toolkit’s low level API to call the service as explained in chapter 5.

As an interop example, I wrote a VB 6 client (in listing 12-13) that uses the low level API to invoke a Web service written in PHP. This example is inspired by a C# example written by my friend Dan Wahlin[3].

 

Listing 12‑13 A VB 6 client that calls a Web service written in PHP. (VBWSClientCode\Chapter12\VB6Client\frmMain.frm)

Private Const OPERATION_NAME As String = "getheaders"

Private Const OPERATION_NS = "nntp.xsd"

Private Const OPERATION_PREFIX = "m"

Private Const XSD_NS As String = "http://www.w3.org/2001/XMLSchema"

Private Const XSI_NS As String = "http://www.w3.org/2001/XMLSchema-instance"

Private Const SOAP_ENC As String = "http://schemas.xmlsoap.org/soap/encoding/"

Private Const SERVICE_URL = "http://www.codecraze.com/soap/nntp.php"

Private Const SOAP_ACTION = """http://www.codecraze.com/soap/nntp.php"""

Private Sub cmdPHP_Click()

On Error GoTo eh

   

    Dim Serializer As SoapSerializer30

    Dim Connector As SoapConnector30

    Dim Rdr As SoapReader30

   

    Set Connector = New HttpConnector30

    Connector.Property("EndPointURL") = SERVICE_URL

    Connector.Property("SoapAction") = SOAP_ACTION

    If chkPrxy.Value Then

        Connector.Property("ProxyServer") = "localhost"

        Connector.Property("ProxyPort") = "8080"

        Connector.Property("UseProxy") = True

    End If

    'establish connection

    Connector.Connect

    Connector.BeginMessage

    'SoapSerializer uses the Connector

    Set Serializer = New SoapSerializer30

    Serializer.Init Connector.InputStream

    'write the SOAP message

    Serializer.StartEnvelope

    Serializer.StartBody

    Serializer.StartElement OPERATION_NAME, _

            OPERATION_NS, _

            SOAP_ENC, _

            OPERATION_PREFIX

    'declare the xsi and xsd namespaces

    Serializer.SoapNamespace "xsi", XSI_NS

    Serializer.SoapNamespace "xsd", XSD_NS

    'write group name

    Serializer.StartElement "newsgroup"

    'add the xsi:type attribute

    Serializer.SoapAttribute "type", XSI_NS, "xsd:string", "xsi"

    Serializer.WriteString "microsoft.public.webservices"

    Serializer.EndElement

    'write password

    Serializer.StartElement "numitems"

    'add the xsi:type attribute

     Serializer.SoapAttribute "type", XSI_NS, "xsd:int", "xsi"

    Serializer.WriteString "10"

    Serializer.EndElement

    'end the operation element

    Serializer.EndElement

    Serializer.EndBody

    Serializer.EndEnvelope

    Serializer.Finished

    'send the message

    Connector.EndMessage

   

    'get the response

    Set Rdr = New SoapReader30

    Rdr.Load Connector.OutputStream

   

    MsgBox Rdr.RpcResult.xml

    Exit Sub

eh:

    MsgBox Err.Description, vbCritical, "Error calling service"

End Sub

This example calls a news Web service on codecraze.com. Specifically, it calls an operation named getheaders which takes in the name of a newsgroup and the maximum number of items to return and returns a list of nntp message headers from that group. The problem with this service is that it requires specific namespace prefixes. That is, it requires that the SOAP envelope prefix be “SOAP-ENV” and the operation element’s prefix to be “m”. Of course this should not be the case: XML namespace prefixes should not matter as long as the namespace URIs they point to are correct.

The code in listing 12-13 starts with a few constant declarations for things like the operation name, service URL, and SOAP Action. The rest of the code in listing 12-13 uses the now-familiar low level API. It first creates an HttpConnector30 object and sets its EndPointURL and SoapAction properties. It then connects to the service and creates a new SoapSerializer30 with the connector’s input stream. After calling StartEnvelope[4] and StartBody, it calls StartElement to write the operation’s element. Note that in this example, the prefix “m” is specified as the operation’s prefix because the Web service expects this particular prefix. Next, two more elements are written to the request message: newsgroup is the newsgroup name and numitems is the maximum number of items to return.

Finally, the message is sent and the response message is loaded into a SoapReader30 object. The returned list of news items is read using the SoapReader30’s RpcResult property which returns an IXMLDOMNode.

There’s really nothing that different about this example compared to other low-level API examples in chapter 5. You just need to understand what the Web service looks for in a request message and use the toolkit’s API to form the message and send it.

 

.NET Clients

.NET Web services framework creators are also active in the interop community which means it is generally possible to call any Web service from a .NET client. In fact, this is easy most of the time unless there are problems with the Web service or the Web service’s WSDL.

Bad WSDL, No Problem

When interoperating with non .NET Web services, it’s common to run into Web services that have no WSDL or whose WSDL cannot be read by VS .NET or wsdl.exe. When this happens, you have to write the proxy code yourself. As an example of this scenario, we’ll call a Web service that returns sunrise/sunset times given longitude/latitude and a date. The Web service’s WSDL document was originally located at http://www.armyaviator.com/cgi-bin/astro.exe/wsdl/IAstro but has since been taken offline. There’s a copy of this WSDL in this chapter’s client code folder in a subfolder named Astro. While the service is no longer online, having a copy of this WSDL allows us to learn how to manually create proxies from an invalid WSDL document.

There are several errors in the service’s WSDL which prevent VS .NET from generating the proxy class for us. Listing 6-18 shows a shortened version of the service’s WSDL where I removed the operations we won’t call.

Listing 12‑14 A WSDL document for a live Web service. This document doesn’t use fully qualified names for WSDL elements and cannot be read by VS .NET or wsdl.exe. (VBWSClientCode\Chapter12\Astro\Astro.wsdl)

<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"

xmlns:xs="http://www.w3.org/2001/XMLSchema" name="IAstroservice"

targetNamespace="http://www.borland.com/soapServices/"

xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap"

xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">

  <message name="GetSunriseInfoRequest">

    <part name="Latitude" type="xs:double"/>

    <part name="Longitude" type="xs:double"/>

    <part name="Year" type="xs:int"/>

    <part name="Month" type="xs:int"/>

    <part name="Day" type="xs:int"/>

    <part name="TimeBiasMinutes" type="xs:int"/>

  </message>

  <message name="GetSunriseInfoResponse">

    <part name="return" type="xs:string"/>

  </message>

 <portType name="IAstro">

    <operation name="GetSunriseInfo">

      <input message="GetSunriseInfoRequest"/>

      <output message="GetSunriseInfoResponse"/>

    </operation>

  </portType>

  <binding name="IAstrobinding" type="IAstro">

    <soap:bindi