|
Building Web ServicesIf 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:
A Currency Conversion Web ServiceOnce 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 WSDLBy 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.
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. Creating a Custom ClientLet'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; Asking for Database DataFor 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 DataAll 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 DocumentsThe 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 := '<employeeList>' + 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 + '</employeeList>'; 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.
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; Debugging the SOAP HeadersOne 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. 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 ServiceAlthough 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:
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. |
|
Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide |
|