Web Services and SOAP

Overview

Of all the recent features of Delphi, one stands out clearly: the support for web services built into the product. The fact that I'm discussing it near the end of the book has nothing to do with its importance, but only with the logical flow of the text, and with the fact that this is not the starting point from which to learn Delphi programming.

The topic of web services is broad and involves many technologies and business-related standards. As usual, I'll focus on the underlying Delphi implementation and the technical side of web services, rather than discuss the larger picture and business implications.

This chapter is also relevant because Delphi 7 adds a lot of power to the implementation of web services provided by Delphi 6, including support for attachments, custom headers, and much more. You'll see how to create a web service client and a web service server, and also how to move database data over SOAP using the DataSnap technology.

Web Services

The rapidly emerging web services technology has the potential to change the way the Internet works for businesses. Browsing web pages to enter orders is fine for individuals (business-to-consumer [B2C] applications) but not for companies (business-to-business [B2B] applications). If you want to buy a few books, going to a book vendor website and punching in your requests is probably fine. But if you run a bookstore and want to place hundreds of orders a day, this is far from a good approach, particularly if you have a program that helps you track your sales and determine reorders. Grabbing the output of this program and reentering it into another application is ridiculous.

Web services are meant to solve this issue: The program used to track sales can automatically create a request and send it to a web service, which can immediately return information about the order. The next step might be to ask for a tracking number for the shipment. At this point, your program can use another web service to track the shipment until it is at its destination, so you can tell your customers how long they have to wait. As the shipment arrives, your program can send a reminder via SMS or pager to the people with pending orders, issue a payment with a bank web service, and … I could continue but I think I've given you the idea. Web services are meant for computer interoperability, much as the Web and e-mail let people interact.

SOAP and WSDL

Web services are made possible by the Simple Object Access Protocol (SOAP). SOAP is built over standard HTTP, so that a web server can handle the SOAP requests and the related data packets can pass though firewalls. SOAP defines an XML-based notation for requesting the execution of a method by an object on the server and passing parameters to it; another notation defines the format of a response.

  Note 

SOAP was originally developed by DevelopMentor (the training company run by COM expert Don Box) and Microsoft, to overcome weaknesses involved with using DCOM in web servers. Submitted to the W3C for standardization, it is being embraced by many companies, with a particular push from IBM. It is too early to know whether there will be standardization to let software programs from Microsoft, IBM, Sun, Oracle, and many others truly interoperate, or whether some of these vendors will try to push a private version of the standard. In any case, SOAP is a cornerstone of Microsoft's .NET architecture and also of the current platforms by Sun and Oracle.

SOAP will replace COM invocation, at least between different computers. Similarly, the definition of a SOAP service in the Web Services Description Language (WSDL) format will replace the IDL and type libraries used by COM and COM+. WSDL documents are another type of XML document that provides the metadata definition of a SOAP request. As you get a file in this format (generally published to define a service), you'll be able to create a program to call it.

Specifically, Delphi provides a bi-directional mapping between WSDL and interfaces. This means you can grab a WSDL file and generate an interface for it. You can then create a client program, embedding SOAP requests via these interfaces, and use a special Delphi component that lets you convert your local interface requests into SOAP calls (I doubt you want to manually generate the XML required for a SOAP request).

The other way around, you can define an interface (or use an existing one) and let a Delphi component generate a WSDL description for it. Another component provides you with a SOAP-to-Pascal mapping, so that by embedding this component and an object implementing the interface within a server-side program, you can have your web service up and running in a matter of minutes.

BabelFish Translations

As a first example of the use of web service, I've built a client for the BabelFish translation service offered by AltaVista. You can find this and many other services for experimentation on the XMethods website (www.xmethods.com).

After downloading the WSDL description of this service from XMethods (also available among the source code files for this chapter), I invoked Delphi's Web Services Importer in the Web Services page of the New Items dialog box and selected the file. The wizard lets you preview the structure of the service (see Figure 23.1) and generate the proper Delphi-language interfaces in a unit like the following (with many of the comments removed):

click to expand
Figure 23.1:  The WSDL Import Wizard in action

unit BabelFishService;
 
interface
 
uses InvokeRegistry, SOAPHTTPClient, Types, XSBuiltIns;
 
type
 BabelFishPortType = interface(IInvokable)
 ['{D2DB6712-EBE0-1DA6-8DEC-8A445595AE0C}']
 function BabelFish(const translationmode: WideString; 
 const sourcedata: WideString): WideString; stdcall;
 end;
 
function GetBabelFishPortType(UseWSDL: Boolean=System.False; 
 Addr: string=''; HTTPRIO: THTTPRIO = nil): BabelFishPortType;
 
implementation
 
// omitted
 
initialization
 InvRegistry.RegisterInterface(TypeInfo(BabelFishPortType), 
 'urn:xmethodsBabelFish', '');
 InvRegistry.RegisterDefaultSOAPAction(TypeInfo(BabelFishPortType),
    'urn:xmethodsBabelFish#BabelFish');
 
end.

Notice that the interface inherits from the IInvokable interface. This interface doesn't add anything in terms of methods to Delphi's IInterface base interface, but it is compiled with the flag used to enable RTTI generation, {$M+}, like the TPersistent class. In the initialization section, you notice that the interface is registered in the global invocation registry (or InvRegistry), passing the type information reference of the interface type.

  Note 

Having RTTI information for interfaces is the most important technological advance underlying SOAP invocation. Not that SOAP-to-Pascal mapping isn't important—it is crucial to simplify the process—but having RTTI for an interface makes the entire architecture powerful and robust.

The third element of the unit generated by the WSDL Import Wizard is a global function named after the service, introduced in Delphi 7. This function helps simplify the code used to call the web service. The GetBabelFishPortType function returns an interface of the proper type, which you can use to issue a call directly. For instance, the following code translates a short sentence from English into Italian (as indicated by the value of the first parameter, en_it) and shows it on screen:

ShowMessage (GetBabelFishPortType.BabelFish('en_it', 'Hello, world!'));

If you look at the code for the GetBabelFishPortType function, you'll see that it creates an internal invocation component of the class THTTPRIO to process the call. You can also place this component manually on the client form (as I've done in the example program) to gain better control over its various settings (and handle its events).

This component can be configured in two basic ways: You can refer to the WSDL file or URL, import it, and extract from it the URL of the SOAP call; or, you can provide a direct URL to call. The example has two components that provide the alternative approaches (with exactly the same effect):

object HTTPRIO1: THTTPRIO
 WSDLLocation = 'BabelFishService.xml'
 Service = 'BabelFish'
 Port = 'BabelFishPort'
end
object HTTPRIO2: THTTPRIO
 URL = 'http://services.xmethods.net:80/perl/soaplite.cgi'
end

At this point, there is little left to do. You have information about the service that can be used for its invocation, and you know the types of the parameters required by the only available method as they are listed in the interface. The two elements are merged by extracting the interface you want to call directly from the HTTPRIO component, with an expression like HTTPRIO1 as BabelFishPortType. It might seem astonishing at first, but it is also outrageously simple.

This is the web service call done by the example:

EditTarget.Text := (HTTPRIO1 as BabelFishPortType).
 BabelFish(ComboBoxType.Text, EditSource.Text);

The program output, shown in Figure 23.2, allows you to learn foreign languages (although the teacher has its shortcomings!). I haven't replicated the same example with stock options, currencies, weather forecasts, and the many other services available, because they would look much the same.

click to expand
Figure 23.2: An example of a translation from English to German obtained by Alta-Vista's BabelFish via a web service

  Warning 

Although the web service interface provides you with the types of the parameters, in many cases you need to refer to some actual documentation of the service to know what the values of the parameters really mean and how they are interpreted by the service. The BabelFish web service is an example of this issue, as I had to look at some textual documentation to find out the list of translation types, available in the demo in a combo box.

Building Web Services

If calling a web service in Delphi is straightforward, the same can be said of developing a service. If you go into the Web Services page of the New Items dialog box, you can see the SOAP Server Application option. Select it, and Delphi presents you with a list that's quite similar to what you see if you select a WebBroker application. A web service is typically hosted by a web server using one of the available web server extension technologies (CGI, ISAPI, Apache modules, and so on) or the Web App Debugger for your initial tests.

After completing this step, Delphi adds three components to the resulting web module, which is just a plain web module with no special additions:

  • The HTTPSoapDispatcher component receives the web request, as any other HTTP dispatcher does.
  • The HTTPSoapPascalInvoker component does the reverse operation of the HTTPRIO component; it can translate SOAP requests into calls of Pascal interfaces (instead of shifting interface method calls into SOAP requests).
  • The WSDLHTMLPublish component can be used to extract the WSDL definition of the service from the interfaces it support, and performs the opposite role of the Web Services Importer Wizard. Technically, this is another HTTP dispatcher.

A Currency Conversion Web Service

Once this framework is in place—something you can also do by adding the three components listed in the previous section to an existing web module—you can begin writing a service. As an example, I've taken the euro conversion example from Chapter 3, "The Run-Time Library," and transformed it into a web service called ConvertService. First, I've added to the program a unit defining the interface of the service, as follows:

type
 IConvert = interface(IInvokable)
  ['{FF1EAA45-0B94-4630-9A18-E768A91A78E2}']
 function ConvertCurrency (Source, Dest: string; Amount: Double): Double;
 stdcall;
 function ToEuro (Source: string; Amount: Double): Double; stdcall;
 function FromEuro (Dest: string; Amount: Double): Double; stdcall;
 function TypesList: string; stdcall;
 end;

Defining an interface directly in code, without having to use a tool such as the Type Library Editor, provides a great advantage, as you can easily build an interface for an existing class and don't have to learn using a specific tool for this purpose. Notice that I've given a GUID to the interface, as usual, and used the stdcall calling convention, because the SOAP converter does not support the default register calling convention.

In the same unit that defines the interface of the service, you should also register it. This operation will be necessary on both the client and server sides of the program, because you will be able to include this interface definition unit in both:

uses InvokeRegistry;
 
initialization
 InvRegistry.RegisterInterface(TypeInfo(IConvert));

Now that you have an interface you can expose to the public, you have to provide an implementation for it, again by means of the standard Delphi code (and with the help of the predefined TInvokableClass class:

type
 TConvert = class (TInvokableClass, IConvert)
 protected
 function ConvertCurrency (Source, Dest: string; Amount: Double): Double;
 stdcall;
 function ToEuro (Source: string; Amount: Double): Double; stdcall;
 function FromEuro (Dest: string; Amount: Double): Double; stdcall;
 function TypesList: string; stdcall;
 end;

The implementation of these functions, which call the code of the euro conversion system from Chapter 3, is not discussed here because it has little to do with the development of the service. However, it is important to notice that this implementation unit also has a registration call in its initialization section:

InvRegistry.RegisterInvokableClass (TConvert);

Publishing the WSDL

By registering the interface, you make it possible for the program to generate a WSDL description. The web service application (since the Delphi 6.02 update) is capable of displaying a first page describing its interfaces and the detail of each interface, and returning the WSDL file. By connecting to the web service via a browser, you'll see something similar to Figure 23.3.

click to expand
Figure 23.3: The description of the Convert-Service web service provided by Delphi components

  Note 

Although other web service architectures automatically provide you with a way to execute the web service from the browser, this technique is mostly meaningless, because using web services makes sense in an architecture where different applications interoperate. If all you need to do is show data on a browser, you should build a website!

This auto-descriptive feature was not available in web services produced in Delphi 6 (which provided only the lower-level WSDL listing), but it is quite easy to add (or customize). If you look at the Delphi 7 SOAP web module you'll notice a default action with an OnAction event handler invoking the following default behavior:

WSDLHTMLPublish1.ServiceInfo(Sender, Request, Response, Handled);

This is all you have to do to retrofit this feature into an existing Delphi web service that lacks it. To provide similar functionality manually, you must call into the invocation registry (the InvRegistry global object), with calls like GetInterfaceExternalName and GetMethExternalName.

What's important is the web service application's ability to document itself to any other programmer or programming tool, by exposing the WSDL.

Creating a Custom Client

Let's move to the client application that calls the service. I don't need to start from the WSDL file, because I already have the Delphi interface. This time the form doesn't even have the HTTPRIO component, which is created in code:

private
 Invoker: THTTPRio;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
 Invoker := THTTPRio.Create(nil);
 Invoker.URL := 'http://localhost/scripts/ConvertService.exe/soap/iconvert';
 ConvIntf := Invoker as IConvert;
end;

As an alternative to using a WSDL file, the SOAP invoker component can be associated with an URL. Once this association has been done and the required interface has been extracted from the component, you can begin writing straight Pascal code to invoke the service, as you saw earlier.

A user fills the two combo boxes, calling the TypesList method, which returns a list of available currencies within a string (separated by semicolons). You extract this list by replacing each semicolon with a line break and then assigning the multiline string directly to the combo items:

procedure TForm1.Button2Click(Sender: TObject);
var
 TypeNames: string;
begin
 TypeNames := ConvIntf.TypesList;
 ComboBoxFrom.Items.Text := StringReplace (TypeNames, ';', sLineBreak,
 [rfReplaceAll]);
 ComboBoxTo.Items := ComboBoxFrom.Items;
end;

After selecting two currencies, you can perform the conversion with this code (Figure 23.4 shows the result):

procedure TForm1.Button1Click(Sender: TObject);
begin
 LabelResult.Caption := Format ('%n', [(ConvIntf.ConvertCurrency(
 ComboBoxFrom.Text, ComboBoxTo.Text, StrToFloat(EditAmount.Text)))]);
end;


Figure 23.4:  The ConvertCaller client of the Convert-Service web service shows how few German marks you used to get for so many Italian liras, before the euro changed everything.

Asking for Database Data

For this example, I built a web service (based on the Web App Debugger) capable of exposing data about employees of a company. This data is mapped to the EMPLOYEE table of sample InterBase database we've used so often throughout the book. The Delphi interface of the web service is defined in the SoapEmployeeIntf unit as follows:

type
 ISoapEmployee = interface (IInvokable)
 ['{77D0D940-23EC-49A5-9630-ADE0751E3DB3}']
 function GetEmployeeNames: string; stdcall;
 function GetEmployeeData (EmpID: string): string; stdcall;
 end;

The first method returns a list of the names of all the employees in the company, and the second returns the details of a given employee. The implementation of this interface is provided in the Soap-EmployeeImpl unit with the following class:

type
 TSoapEmployee = class(TInvokableClass, ISoapEmployee)
 public
 function GetEmployeeNames: string; stdcall;
 function GetEmployeeData (EmpID: string): string; stdcall;
 end;

The implementation of the web service lies in the two previous methods and some helper functions to manage the XML data being returned. But before we get to the XML portion of the example, let me briefly discuss the database access section.

Accessing the Data

All the connectivity and SQL code in this example are hosted in a separate data module. Of course, I could have created some connection and dataset components dynamically in the methods, but doing so is contrary to the approach of a visual development tool like Delphi. The data module has the following structure:

object DataModule3: TDataModule3
 object SQLConnection: TSQLConnection
 ConnectionName = 'IBConnection'
 DriverName = 'Interbase'
 LoginPrompt = False
 Params.Strings = // omitted 
 end
  object dsEmplList: TSQLDataSet
 CommandText = 'select EMP_NO, LAST_NAME, FIRST_NAME from EMPLOYEE'
 SQLConnection = SQLConnection
 object dsEmplListEMP_NO: TStringField
 object dsEmplListLAST_NAME: TStringField
 object dsEmplListFIRST_NAME: TStringField
 end
 object dsEmpData: TSQLDataSet
 CommandText = 'select * from EMPLOYEE where Emp_No = :id'
 Params = <
 item
 DataType = ftFixedChar
 Name = 'id'
 ParamType = ptInput
 end>
 SQLConnection = SQLConnection
  end
end

As you can see, the data module has two SQL queries hosted by SQLDataSet components. The first is used to retrieve the name and ID of each employee, and the second returns the entire set of data for a given employee.

Passing XML Documents

The problem is how to return this data to a remote client program. In this example, I've used the approach I like best: I've returned XML documents, instead of working with complex SOAP data structures. (I don't get how XML can be seen as a messaging mechanism for SOAP invocation—along with the transport mechanism provided by HTTP—but then, it is not used for the data being transferred. Still, very few web services return XML documents, so I'm beginning to wonder if it's me or many other programmers who can't see the full picture.)

In this example, the GetEmployeeNames method creates an XML document containing a list of employees, with their first and last names as values and the related database ID as an attribute, using two helper functions MakeXmlStr (already described in the last chapter) and MakeXmlAttribute (listed here):

function TSoapEmployee.GetEmployeeNames: string;
var
 dm: TDataModule3;
begin
 dm := TDataModule3.Create (nil);
 try
 dm.dsEmplList.Open;
 Result := '' + sLineBreak;
 while not dm.dsEmplList.EOF do
 begin
 Result := Result + ' ' + MakeXmlStr ('employee',
 dm.dsEmplListLASTNAME.AsString + ' ' +
 dm.dsEmplListFIRSTNME.AsString,
 MakeXmlAttribute ('id', dm.dsEmplListEMPNO.AsString)) + sLineBreak;
 dm.dsEmplList.Next;
 end;
 Result := Result + '';
 finally
 dm.Free;
 end;
end;
 
function MakeXmlAttribute (attrName, attrValue: string): string;
begin
 Result := attrName + '="' + attrValue + '"';
end;

Instead of the manual XML generation, I could have used the XML Mapper or some other technology; but as you should know from Chapter 22 ("Using XML Technologies"), I rather prefer creating XML directly in strings. I'll use the XML Mapper to process the data received on the client side.

  Note 

You may wonder why the program creates a new instance of the data module each time. The negative side of this approach is that each time, the program establishes a new connection to the database (a rather slow operation); but the plus side is that you have no risk related to the use of a multithreaded application. If two web service requests are executed concurrently, you can use a shared connection to the database, but you must use different dataset components for the data access. You could move the datasets in the function code and keep only the connection on the data module, or have a global shared data module for the connection (used by multiple threads) and a specific instance of a second data module hosting the datasets for each method call.

Let's now look at the second method, GetEmployeeData. It uses a parametric query and formats the resulting fields in separate XML nodes (using another helper function, FieldsToXml):

function TSoapEmployee.GetEmployeeData(EmpID: string): string;
var
 dm: TDataModule3;
begin
 dm := TDataModule3.Create (nil);
 try
 dm.dsEmpData.ParamByName('ID').AsString := EmpId;
 dm.dsEmpData.Open;
 Result := FieldsToXml ('employee', dm.dsEmpData);
 finally
 dm.Free;
 end;
end;
 
function FieldsToXml (rootName: string; data: TDataSet): string;
var
 i: Integer;
begin
 Result := '<' + rootName + '>' + sLineBreak;;
 for i := 0 to data.FieldCount - 1 do
 Result := Result + ' ' + MakeXmlStr (
 LowerCase (data.Fields[i].FieldName),
 data.Fields[i].AsString) + sLineBreak;
 Result := Result + ' + rootName + '>' + sLineBreak;;
end;

The Client Program (with XML Mapping)

The final step for this example is to write a test client program. You can do so as usual by importing the WSDL file defining the web service. In this case, you also have to convert the XML data you receive into something more manageable—particularly the list of employees returned by the GetEmployeeNames method. As mentioned earlier, I've used Delphi's XML Mapper to convert the list of employees received from the web service into a dataset I can visualize using a DBGrid.

To accomplish this, I first wrote the code to receive the XML with the list of employees and copied it into a memo component and from there to a file. Then, I opened the XML Mapper, loaded the file, and generated from it the structure of the data packet and the transformation file. (You can find the transformation file among the source code files of the SoapEmployee example.) To show the XML data within a DBGrid, the program uses an XMLTransformProvider component, referring to the transformation file:


object XMLTransformProvider1: TXMLTransformProvider
 TransformRead.TransformationFile = 'EmplListToDataPacket.xtr'
end

The ClientDataSet component is not hooked to the provider, because it would try to open the XML data file specified by the transformation. In this case, the XML data doesn't reside in a file, but is passed to the component after calling the web service. For this reason the program moves the data to the ClientDataSet directly in code:


procedure TForm1.btnGetListClick(Sender: TObject);
var
 strXml: string;
begin
 strXml := GetISoapEmployee.GetEmployeeNames;
 strXML := XMLTransformProvider1.TransformRead.TransformXML(strXml);
 ClientDataSet1.XmlData := strXml;
 ClientDataSet1.Open;
end;

With this code, the program can display the list of employees in a DbGrid, as you can see in Figure 23.5. When you retrieve the data for the specific employee, the program extracts the ID of the active record from the ClientDataSet and then shows the resulting XML in a memo:


procedure TForm1.btnGetDetailsClick(Sender: TObject);
begin
 Memo2.Lines.Text := GetISoapEmployee.GetEmployeeData(
 ClientDataSet1.FieldByName ('id').AsString);
end;

click to expand
Figure 23.5: The client program of the SoapEmployee web service example

Debugging the SOAP Headers

One final note for this example relates to the use of the Web App Debugger for testing SOAP applications. Of course, you can run the server program from the Delphi IDE and debug it easily, but you can also monitor the SOAP headers passed on the HTTP connection. Although looking at SOAP from this low-level perspective can be far from simple, it is the ultimate way to check if something is wrong with either a server or a client SOAP application. As an example, in Figure 23.6 you can see the HTTP log of a SOAP request from the last example.

click to expand
Figure 23.6:  The HTTP log of the Web App Debugger includes the low-level SOAP request.

The Web App Debugger might not always be available, so another common technique is to handle the events of the HTTPRIO component, as the BabelFishDebug example does. The program's form has two memo components in which you can see the SOAP request and the SOAP response:


procedure TForm1.HTTPRIO1BeforeExecute(const MethodName: String;
 var SOAPRequest: WideString);
begin
 MemoRequest.Text := SoapRequest;
end;
 
procedure TForm1.HTTPRIO1AfterExecute(const MethodName: String;
 SOAPResponse: TStream);
begin
 SOAPResponse.Position := 0;
 MemoResponse.Lines.LoadFromStream(SOAPResponse);
end;

Exposing an Existing Class as a Web Service

Although you might want to begin developing a web service from scratch, in some cases you may have existing code to make available. This process is not too complex, given Delphi's open architecture in this area. To try it, follow these steps:

  1. Create a web service application or add the related components to an existing WebBroker project.
  2. Define an interface inheriting from IInvokable and add to it the methods you want to make available in the web service (using the stdcall calling convention). The methods will be similar to those of the class you want to make available.
  3. Define a new class that inherits from the class you want to expose and implements your interface. The methods will be implemented by calling the corresponding methods of the base class.
  4. Write a factory method to create an object of your implementation class any time a SOAP request needs it.

This last step is the most complex. You could define a factory and register it as follows:


procedure MyObjFactory (out Obj: TObject);
begin
 Obj := TMyImplClass.Create;
end;
 
initialization
 InvRegistry.RegisterInvokableClass(TMyImplClass, MyObjFactory);

However, this code creates a new object for every call. Using a single global object would be equally bad: Many different users might try to use it, and if the object has state or its methods are not concurrent, you might be in for big problems. You're left with the need to implement some form of session management, which is a variation on the problem we had with the earlier web service connecting to the database.

DataSnap over SOAP

Now that you have a reasonably good idea how to build a SOAP server and a SOAP client, let's look at how to use this technology in building a multitier DataSnap application. You'll use a Soap Server Data Module to create the new web service and the SoapConnection component to connect a client application to it.

Building the DataSnap SOAP Server

Let's look at the server side first. Go to the Web Services page of the New Items dialog box and use the Soap Server Application icon to create a new web service, and then use the Soap Server Data Module icon to add a DataSnap server-side data module to the SOAP server. I did this in the SoapDataServer7 example (which uses the Web App Debugger architecture for testing purposes). From this point on, all you do is write a normal DataSnap server (or a middle-tier DataSnap application), as discussed in Chapter 16 ("Multitier DataSnap Applications"). In this case, I added InterBase access to the program by means of dbExpress, resulting in the following structure:


object SoapTestDm: TSoapTestDm
  object SQLConnection1: TSQLConnection
 ConnectionName = 'IBLocal'
  end
  object SQLDataSet1: TSQLDataSet
 SQLConnection = SQLConnection1
 CommandText = 'select * from EMPLOYEE'
  end
  object DataSetProvider1: TDataSetProvider
 DataSet = SQLDataSet1
  end
end

The data module built for a SOAP-based DataSnap server defines a custom interface (so you can add methods to it) inheriting from IAppServerSOAP, which is defined as a published interface (even though it doesn't inherit from IInvokable).

  Tip 

Delphi 6 used DataSnap's standard IAppServer interface for exposing data via SOAP. Delphi 7 has replaced the default with the inherited IAppServerSOAP interface, which is functionally identical but allows the system to discriminate the type of call depending on the interface name. You'll see shortly how to call an older application from a client built in Delphi 7, because this process is not automatic.

The implementation class, TSoapTestDm, is the data module, as in other DataSnap servers. Here is the code Delphi generated, with the addition of the custom method:


type
 ISampleDataModule = interface(IAppServerSOAP)
 ['{D47A293F-4024-4690-9915-8A68CB273D39}']
 function GetRecordCount: Integer; stdcall;
 end;
 
 TSampleDataModule = class(TSoapDataModule, ISampleDataModule,
 IAppServerSOAP, IAppServer)
 DataSetProvider1: TDataSetProvider;
 SQLConnection1: TSQLConnection;
 SQLDataSet1: TSQLDataSet;
  public
 function GetRecordCount: Integer; stdcall;
 end;

The base TSoapDataModule doesn't inherit from TInvokableClass. This is not a problem as long as you provide an extra factory procedure to create the object (which is what TInvokableClass does for you) and add it to the registration code (as discussed earlier, in the section "Exposing an Existing Class as a Web Service"):


procedure TSampleDataModuleCreateInstance(out obj: TObject);
begin
 obj := TSampleDataModule.Create(nil);
end;
 
initialization
 InvRegistry.RegisterInvokableClass(
 TSampleDataModule, TSampleDataModuleCreateInstance);
 InvRegistry.RegisterInterface(TypeInfo(ISampleDataModule));

The server application also publishes the IAppServerSOAP and IAppServer interfaces, thanks to the (little) code in the SOAPMidas unit. As a comparison, you can find a SOAP DataSnap server built with Delphi 6 in the SoapDataServer folder. The example can still be compiled in Delphi 7 and works fine, but you should write new programs using the structure of the newer; an example is in the SoapDataServer7 folder.

  Tip 

Web service applications in Delphi 7 can include more than one SOAP data module. To identify a specific SOAP data module, use the SOAPServerIID property of the SoapConnection component or add the data module interface name to the end of the URL.

The server has a custom method (the Delphi 6 version of the program also had one, but it never worked) that uses a query with the select count(*) from EMPLOYEE SQL statement:


function TSampleDataModule.GetRecordCount: Integer;
begin
 // read in the record count by running a query
 SQLDataSet2.Open;
 Result := SQLDataSet2.Fields[0].AsInteger;
 SQLDataSet2.Close;
end;

Building the DataSnap SOAP Client

To build the client application, called SoapDataClient7, I began with a plain program and added a SoapConnection component to it (from the Web Services page of the palette), hooking it to the URL of the DataSnap web service and referring to the specific interface I'm looking for:


object SoapConnection1: TSoapConnection
 URL = 'http://localhost:1024/SoapDataServer7.soapdataserver/' +
 'soap/Isampledatamodule'
 SOAPServerIID = 'IAppServerSOAP - {C99F4735-D6D2-495C-8CA2-E53E5A439E61}'
 UseSOAPAdapter = False
end

Notice the last property, UseSOAPAdapter, which indicates you are working against a server built with Delphi 7. As a comparison, the SoapDataClient (again, no 7) example, which uses a server created with Delphi 6 and recompiled with Delphi 7, must have this property set to True. This value forces the program to use the plain IAppServer interface instead of the new IAppServerSOAP interface.

From this point on, you proceed as usual, adding a ClientDataSet component, a DataSource, and a DBGrid to the program; choosing the only available provider for the client dataset; and hooking the rest. Not surprisingly, for this simple example, the client application has little custom code: a single call to open the connection when a button is clicked (to avoid startup errors) and an ApplyUpdates call to send changes back to the database.

SOAP versus Other DataSnap Connections

Regardless of the apparent similarity of this program to all the other DataSnap client and server programs built in Chapter 16, there is a very important difference worth underlining: The SoapDataServer and SoapDataClient programs do not use COM to expose or call the IAppServerSOAP interface. Quite the opposite—the socket- and HTTP-based connections of DataSnap still rely on local COM objects and a registration of the server in the Windows Registry. The native SOAP-based support, however, allows for a totally custom solution that's independent of COM and that offers many more chances to be ported to other operating systems. (You can recompile this server in Kylix, but not the programs built in Chapter 16.)

The client program can also call the custom method I've added to the server to return the record count. This method could be used in a real-world application to show only a limited number of records but inform the user how many haven't yet been downloaded from the server. The client code to call the method relies on an extra HTTPRIO component:


procedure TFormSDC.Button3Click(Sender: TObject);
var
 SoapData: ISampleDataModule;
begin
 SoapData := HttpRio1 as ISampleDataModule;
 ShowMessage (IntToStr (SoapData.GetRecordCount));
end;

Handling Attachments

One of the most important features Borland added to Delphi 7 is full support for SOAP attachments. Attachments in SOAP allow you to send data other than XML text, such as binary files or images. In Delphi, attachments are managed through streams. You can read or indicate the type of attachment encoding, but the transformation of a raw stream of bytes into and from a given encoding is up to your code. This process isn't too complex, though, if you consider that Indy includes a number of encoding components.

As an example of how to use attachments, I've written a program that forwards the binary content of a ClientDataSet (also hosts images) or one of the images alone. The server has the following interface:


type
 ISoapFish = interface(IInvokable)
 ['{4E4C57BF-4AC9-41C2-BB2A-64BCE470D450}']
 function GetCds: TSoapAttachment; stdcall;
 function GetImage(fishName: string): TSoapAttachment; stdcall;
 end;

The implementation of the GetCds method uses a ClientDataSet that refers to the classic BIOLIFE table, creates a memory stream, copies the data to it, and then attaches the stream to the TSoapAttachment result:


function TSoapFish.GetCds: TSoapAttachment; stdcall;
var
 memStr: TMemoryStream;
begin
 Result := TSoapAttachment.Create;
 memStr := TMemoryStream.Create;
 WebModule2.cdsFish.SaveToStream(MemStr); // binary
 Result.SetSourceStream (memStr, soReference);
end;

On the client side, I prepared a form with a ClientDataSet component connected to a DBGrid and a DBImage. All you have to do is grab the SOAP attachment, save it to a temporarily in-memory stream, and then copy the data from the memory stream to the local ClientDataSet:


procedure TForm1.btnGetCdsClick(Sender: TObject);
var
 sAtt: TSoapAttachment;
 memStr: TMemoryStream;
begin
 nRead := 0;
 sAtt := (HttpRio1 as ISoapFish).GetCds;
 try
 memStr := TMemoryStream.Create;
 try
 sAtt.SaveToStream(memStr);
 memStr.Position := 0;
 ClientDataSet1.LoadFromStream(MemStr);
 finally
 memStr.Free;
 end;
 finally
 DeleteFile (sAtt.CacheFile);
 sAtt.Free;
 end;
end;
  Warning 

By default, SOAP attachments received by a client are saved to a temporary file, referenced by the CacheFile property of the TSOAPAttachment object. If you don't delete this file it will remain in a folder that hosts temporary files.

This code produces the same visual effect as a client application loading a local file into a ClientDataSet, as you can see in Figure 23.7. In this SOAP client I used an HTTPRIO component explicitly to be able to monitor the incoming data (which will possibly be very large and slow); for this reason, I set a global nRead variable to zero before invoking the remote method. In the OnReceivingData event of the HTTPRIO object's HTTPWebNode property, you add the data received to the nRead variable. The Read and Total parameters passed to the event refer to the specific block of data sent over a socket, so they are almost useless by themselves to monitor progress:


procedure TForm1.HTTPRIO1HTTPWebNode1ReceivingData(
 Read, Total: Integer);
begin
 Inc (nRead, Read);
 StatusBar1.SimpleText := IntToStr (nRead);
 Application.ProcessMessages;
end;

click to expand
Figure 23.7: The FishClient example receives a binary ClientDataSet within a SOAP attachment.

Supporting UDDI

The increasing popularity of XML and SOAP opens the way for business-to-business communication applications to interoperate. XML and SOAP provide a foundation, but they are not enough—standardization in the XML formats, in the communication process, and in the availability of information about a business are all key elements of a real-world solution.

Among the standards proposed to overcome this situation, the most notable are Universal Description, Discovery, and Integration (UDDI, www.uddi.org) and Electronic Business using eXtensible Markup Language (ebXML, www.ebxml.org). These two solutions partially overlap and partially diverge and are now being further worked on by the OASIS consortium (Organization for the Advancement of Structured Information Standards, www.oasis-open.org). I won't get into the problems with business processes; instead I'll only discuss some of the technical elements of UDDI, because it is specifically supported by Delphi 7.

What Is UDDI?

The Universal Description, Discovery, and Integration (UDDI) specification is an effort to create a catalog of web services offered by companies throughout the world. The goal of this initiative is to build an open, global, platform-independent framework to let business entities find each other, define how they interact with the Internet network, and share a global business registry. Of course, the idea is to speed up the adoption of e-commerce, in the form of B2B applications.

UDDI is basically a global business registry. Companies can register themselves on the system, describing their organization and the web services they offer (in UDDI the term web services is used in a very wide sense, including e-mail addresses and websites). The information in the UDDI registry for each company is divided into three areas:

White Pages  Include contact information, addresses, and the like.

Yellow Pages  Register the company in one or more taxonomies, including industrial categories, products sold by the company, geographical information, and other (possibly customizable) taxonomies.

Green Pages  List the web services offered by the company. Each service is listed under a service type (called a tModel), which can be predefined or a type specifically described by the company (for example, in terms of WSDL).

Technically, the UDDI registry should be perceived like today's DNS, and should have a similar distributed nature: multiple servers, mirrored and caching data. Clients can cache data following given rules.

UDDI defines specific data models for a business entity, a business service, and a binding template. The BusinessEntity type includes core information about the business, such as its name, the category it belongs to, and contact information. It supports the taxonomies of the yellow pages, with industry information, product types, and geographic details.

The BusinessService type includes descriptions of web services (used for the green pages). The main type is only a container for the related services. The services can be bound to a taxonomy (geographical area, product, and so on). Every BusinessService structure includes one or more BindingTemplates (the reference to the service).

The BindingTemplate has a tModel. The tModel includes information about formats, protocols, and security, and references to technical specifications (possibly using the WSDL format). If multiple companies share a tModel, a program can interact with all of them with the same code. A given business program, for example, can offer a tModel for other software programs to interact with it, regardless of the company that has adopted the software.

The UDDI API is based on SOAP. Using SOAP, you can both register data and query a registry. Microsoft also offers a COM-based SDK, and IBM has a Java Open Source toolkit for UDDI. UDDI APIs include inquiry (find_xx and get_xx) and publishing (save_xx and delete_xx) on each of the four core data structures (businessEntity, businessService, bindingTemplate, and tModel).

UDDI in Delphi 7

Delphi 7 includes a UDDI browser you can use to find a web service while importing a WSDL file. The UDDI browser, shown in Figure 23.8, is activated by the WSDL Import Wizard. This browser uses only UDDI version 1 servers (a newer interface is available, but it isn't supported) and has a few predefined UDDI registries. You can add predefined settings in the UDDIBrow.ini file in the Delphi bin folder.

click to expand
Figure 23.8:  The UDDI Browser embedded in the Delphi IDE

This is a handy way to access web services information, but it is not all that Delphi provides. Although the UDDI Browser is not available as a stand-alone application, the UDDI interface units are available (and they are not trivial to import). So, you can write your own UDDI browser.

I'll sketch a simple solution, which is a starting point for a full-blown UDDI browser. The UddiInquiry example, shown in action in Figure 23.9, has a number of features, but not all of them work smoothly (particularly the category search features). The reason lies in the fact that using UDDI implies navigating complex data structures, which are not always mapped in the most obvious way by the WSDL importer. This makes the example code fairly involved; so, I'll show you only the code for a plain search, and not even all of it (another reason is that some readers may not be particularly interested in UDDI).

click to expand
Figure 23.9:  The UddiInquiry example features a limited UDDI browser.

As the program starts, it binds the HTTPRIO component it hosts with the InquireSoap UDDI interface, defined in the inquire_v1 unit provided with Delphi 7:


procedure TForm1.FormCreate(Sender: TObject);
begin
 httprio1.Url := comboRegistry.Text;
 inftInquire := httprio1 as InquireSoap;
end;

Clicking the Search button makes the program call the find_business UDDI API. Because most UDDI functions require many parameters, it has been imported using a single record-based parameter of type FindBusiness; it returns a businessList2 object:


procedure TForm1.btnSearchClick(Sender: TObject);
var
 findBusinessData: Findbusiness;
 businessListData: businessList2;
begin
 httprio1.Url := comboRegistry.Text;
 
 findBusinessData := FindBusiness.Create;
 findBusinessData.name := edSearch.Text;
 findBusinessData.generic := '1.0';
 findBusinessData.maxRows := 20;
 
 businessListData := inftInquire.find_business(findBusinessData);
 BusinessListToListView (businessListData);
 findBusinessData.Free;
end;

The businessList2 object is a list that is processed by the businessListToListView method of the program's main form, showing the most relevant details in a list view component:


procedure TForm1.businessListToListView(businessList: businessList2);
var
 i: Integer;
begin
 ListView1.Clear;
 for i := 0 to businessList.businessInfos.Len do
 begin
 with ListView1.Items.Add do
 begin
 Caption := businessList.businessInfos [i].name;
 SubItems.Add (businessList.businessInfos [i].description);
 SubItems.Add (businessList.businessInfos [i].businessKey);
 end;
  end;
end;

By double-clicking on a list view item you can further explore its details, although the program shows the resulting XML information in a plain textual format (or a TWebBrowser-based XML view) and doesn't further process it. As mentioned, I don't want to get into technical details; if you're interested, you can find them by looking at the source code.

What s Next?

In this chapter, I've focused on web services, covering SOAP, WSDL, and UDDI. Refer to the W3C website and to the UDDI (www.uddi.org) and ebXML (www.ebxml.org) sites for more information in the area of business-oriented web services. I didn't delve much into these non-technical issues, but they were worth mentioning.

You should have noticed in this chapter that Delphi is strong player in the area of web services, with a powerful, open architecture. You can use web services to interact with applications written for the Microsoft .NET platform; Delphi has much to offer for this architecture, because it includes a Delphi for .NET preview (covered in the two final chapters of the book).

Chapter 24, "The Microsoft .NET Architecture from the Delphi Perspective," is focused on the .NET platform; Chapter 25, "Delphi for .NET Preview: The Language and the RTL," covers the new Delphi compiler.

Chapter 24 The Microsoft NET Architecture from the Delphi Perspective



Mastering Delphi 7
Mastering Delphi 7
ISBN: 078214201X
EAN: 2147483647
Year: 2006
Pages: 279

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net