Building Web Services
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 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.
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}']
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
protected
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);
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.
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:
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.
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;
begin
Invoker := THTTPRio.Create(nil);
Invoker.URL := 'http://localhost/scripts/ConvertService.exe/soap/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:
var
TypeNames: string;
begin
TypeNames := ConvIntf.TypesList;
[rfReplaceAll]);
ComboBoxTo.Items := ComboBoxFrom.Items;
end;
After selecting two currencies, you can perform the conversion with this code (Figure 23.4
shows the result):
begin
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.
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
['{77D0D940-23EC-49A5-9630-ADE0751E3DB3}']
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
public
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.
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:
ConnectionName = 'IBConnection'
DriverName = 'Interbase'
LoginPrompt = False
Params.Strings = // omitted
end
SQLConnection = SQLConnection
end
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.
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):
var
dm: TDataModule3;
begin
dm := TDataModule3.Create (nil);
try
dm.dsEmplList.Open;
begin
dm.dsEmplListFIRSTNME.AsString,
MakeXmlAttribute ('id', dm.dsEmplListEMPNO.AsString)) + sLineBreak;
dm.dsEmplList.Next;
end;
finally
dm.Free;
end;
end;
begin
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):
var
dm: TDataModule3;
begin
dm := TDataModule3.Create (nil);
try
dm.dsEmpData.ParamByName('ID').AsString := EmpId;
dm.dsEmpData.Open;
finally
dm.Free;
end;
end;
var
i: Integer;
begin
for i := 0 to data.FieldCount - 1 do
LowerCase (data.Fields[i].FieldName),
data.Fields[i].AsString) + sLineBreak;
end;
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:
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:
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:
begin
Memo2.Lines.Text := GetISoapEmployee.GetEmployeeData(
ClientDataSet1.FieldByName ('id').AsString);
end;
Figure 23.5: The client program of the SoapEmployee web service example
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.
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:
begin
MemoRequest.Text := SoapRequest;
end;
SOAPResponse: TStream);
begin
SOAPResponse.Position := 0;
MemoResponse.Lines.LoadFromStream(SOAPResponse);
end;
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:
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:
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