The XML Newsfeed Example

[Previous] [Next]

In the XML newsfeed example, we're dealing with a site that maintains a database of news articles that are of interest to the XML implementor. This site is available to the public at http://architag.com/xmlnews.html. Clicking on the URL invokes a Microsoft Active Server Pages (ASP) document, which connects to the database of news and creates HTML output that is sent to the user.

Suppose we wanted to have access to the information in the newsfeed database, but we wanted only the data, not the HTML that was created for consumption by a Web browser. In other words, we just want the raw news so that we can format it to suit our needs. To get to that data, we'll use SOAP.

The SOAP Client Application

First we're going to build a client that requests a Web service. We'll create this client as an HTML page that uses JScript in a browser. Figure 8-3, which is described in Table 8-1, shows how the client fits into our example. Figure 8-4 shows how the client looks in the browser. The HTML code in Listing 8-4 shows the body of the HTML document (getXMLNews.htm) we will use. The JScript follows in Listing 8-5. You can find the complete listings for this example on the companion CD at \Samples\Ch08\.

click to view at full size.

Figure 8-3. Create a SOAP request document in the browser, post it to a SOAP server, process the request, and send back a SOAP response document. This process is described in Table 8-1.

click to view at full size.

Figure 8-4. The completed getXMLNews.htm page displayed in a Web browser.

Listing 8-4. The initial HTML code used to create a SOAP client interface as described in Step 1 of Table 8-1.

 

getXMLNews.htm

<HTML><HEAD> </HEAD> <BODY STYLE="font-family:Verdana;"> <HR> <TABLE FRAME="VOID" BORDER="1" RULES="ROWS" WIDTH="100%"> <TR> <TD WIDTH="80" VALIGN="TOP"><B>Days</B></TD> <TD VALIGN="TOP"><INPUT TYPE="TEXT" SIZE="5" ID="newsDays" VALUE="23"></TD> </TR> <TR> <TD WIDTH="80" VALIGN="TOP"><B>Format</B></TD> <TD VALIGN="TOP"><SELECT ID="newsFormat"> <OPTION VALUE="XML">Raw XML</OPTION> <OPTION SELECTED VALUE="HTML3">HTML 3</OPTION> <OPTION VALUE="HTML4">HTML 4 - Netscape</OPTION> <OPTION VALUE="HTML4">HTML 4 - Explorer</OPTION> <OPTION VALUE="xHTML">xHTML</OPTION> <OPTION VALUE="Text">Text</OPTION> </SELECT> </TD> </TR> <TR> <TD WIDTH="80" VALIGN="TOP"><B>Fields</B></TD> <TD VALIGN="TOP"> <INPUT CHECKED TYPE="CHECKBOX" NAME="newsFields" ID="fldHeadline" VALUE="headline">Headline</INPUT> <BR><INPUT CHECKED TYPE="CHECKBOX" NAME="newsFields" ID="fldLocation" VALUE="location">Location</INPUT> <BR><INPUT CHECKED TYPE="CHECKBOX" NAME="newsFields" ID="fldAbstract" VALUE="abstract">Abstract</INPUT> <BR><INPUT CHECKED TYPE="CHECKBOX" NAME="newsFields" ID="fldURL" VALUE="url">URL</INPUT> </TD> </TR> </TABLE> <HR> <BR><INPUT TYPE="BUTTON" VALUE="Get News" ONCLICK="ResultDiv.innerHTML=getXMLNews();"> <H2>SOAP Request</H2> <DIV ID="SOAPDiv" STYLE="border:1pt black solid;">&nbsp;</DIV> <H2>SOAP Response</H2> <DIV ID="ResultDiv" STYLE="border:1pt black solid;">&nbsp;</DIV> </BODY> </HTML>

Table 8-1. A description of the SOAP request process illustrated in Figure 8-3.

Step Description
1 First we create a SOAP client in the form of a Web page. A hard-coded HTML page is probably not what you would normally use, so consider this a learning tool. In actual practice, you would probably initiate a SOAP request from a server program that retrieves information from a number of different sources.
2 The SOAP document is sent over an HTTP connection as a POST command. In this example I use localhost, which points to the Web server on a local machine.
3 The SOAP server receives the request. The request is a CGI program written in OmniMark. The server reads the SOAP request and determines whether the method name is something it knows about. If the server recognizes the method,…
4 …the server opens a TCP connection to the appropriate service and sends the SOAP document to the service.
5 The service is another OmniMark program running in a console window, waiting for activity to happen at a particular port. The service does what it needs to do and returns a package to the server.
6 The server composes a SOAP response package, encapsulating the results from the service.
7 The response document is sent to the waiting client as an HTTP response package.

To complete this page, we need to ask the Web service for a collection of items from an XML newsfeed. Later on, we'll write a Web service that responds to this request.

The getXMLNews.htm file lets users request information we have stored on a Web server. They can indicate the format they want this news in, and they can specify the number of days back to search the database. Our JScript will process the user request and indicate how far back from the current day we want to receive items, what the format of the information should be, and which fields we want to include in the response.

The Get News button will set in motion the following process:

  1. Execute a JScript function that creates a SOAP document with a payload that contains a request that will be sent to a Web service.
  2. This SOAP document is sent to the Web service via an HTTP POST command.
  3. The Web service performs the following tasks, in the order indicated:
    • Reads the SOAP document, extracting the payload
    • Parses the request payload to determine the parameters of the method invocation
    • Queries the database based on the parameters indicated
    • Prepares a response packet
    • Wraps the response packet as a SOAP response object
    • Ships the response packet back to the browser as an HTTP response

  4. The JScript function receives the response and transforms it with an XSLT style sheet.
  5. Listing 8-5 shows the function for sending the POST command to our SOAP server. Table 8-2 offers a line-by-line description of the code in Listing 8-5. You can see the complete Web page in Figure 8-4.

Listing 8-5. JScript used to post an XML document to a SOAP server.

 

getXMLNews# POST Function

1 <SCRIPT LANGUAGE="JScript"> 2 var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); 3 4 var objStyle = new ActiveXObject("MSXML2.DOMDocument"); 5 objStyle.async = false; 6 objStyle.load("ShowXML.xsl") 7 8 var SOAPRequest = new ActiveXObject("MSXML2.DOMDocument"); 9 SOAPRequest.async = false; 10 11 var SOAPResponse = new ActiveXObject("MSXML2.DOMDocument"); 12 SOAPResponse.async = false; 13 14 function getXMLNews() 15 { 16 xmlhttp.Open("POST", 17 "http://architag.com/scripts/SOAPServer.xar", false); 18 strXML = 19 "<SOAP:Envelope " + 20 " xmlns:SOAP='urn:schemas-xmlsoap-org:soap.v1'>" + 21 " <SOAP:Body>" + 22 " <getXMLNewsRequest" + 23 " xmlns='urn:schemas-architag-com:getXMLNews'>" + 24 " <days>" + document.all.newsDays.value + "</days>" + 25 " <format>" + document.all.newsFormat.value + "</format>" + 26 " <fields>" 27 if (fldHeadline.checked) strXML += "headline " 28 if (fldLocation.checked) strXML += "location " 29 if (fldAbstract.checked) strXML += "abstract " 30 if (fldURL.checked) strXML += "url " 31 strXML += " </fields>" + 32 " </getXMLNewsRequest>" + 33 " </SOAP:Body>" + 34 "</SOAP:Envelope>" 35 36 SOAPRequest.loadXML(strXML) 37 SOAPDiv.innerHTML = 38 SOAPRequest.transformNode(objStyle.documentElement) 39 xmlhttp.setRequestHeader("SOAPMethodName", "getXMLNewsRequest") 40 xmlhttp.Send(SOAPRequest.xml); 41 SOAPResponse.loadXML(xmlhttp.responseXML.xml) 42 return SOAPResponse.transformNode(objStyle.documentElement) 43 } 44 </SCRIPT>

Table 8-2. Line-by-line description of the JScript code in Listing 8-5.

Line Description
1 Beginning of the JScript block.
2 We use ActiveXObject to instantiate an XMLHTTP object.
4 We need to create several instances of the XML parser. First we create a copy that will hold our XSL style sheet.
5 The async property controls how the parser behaves as it loads the document. If we set the async property to false, the parser will wait until the document is loaded or until there is an error. If we set the async property to true, the parser will fire off an asynchronous process. Setting async to true is nice because we can load the document in the background. However, it complicates the program because we need to indicate the function that should execute when the load is finished. For now, let's simplify things and set the async property to false.
6 The load method loads an XML document from the URL indicated in the argument. In this case, the document is our XSL style sheet in the current directory.
8-9 A second instance of the XML processor will eventually hold our SOAP request document.
11-12 A third instance of the XML processor will hold the response document that comes back from the SOAP server.
14 The getXMLNews function will be called from the Web page.
16-17 The Open method of the XMLHTTP object creates a connection to the server. In this case, the server is an OmniMark program located on the Architag server in Colorado. The second argument is the async property, which works just like the async property on the XML parser object. We want to set this property to false, to force the program to wait until it receives the response.
18-34 The strXML = line builds the XML document as a serialized string, inserting values from the fields on the Web page. This process is known as "XML by concatenation." We could also build the XML document by inserting element and attribute nodes with the DOM API. That technique probably provides a more robust program, but I think the concatenation approach is easier to use when you are starting out.
36 Once we create the XML document, we load it into the XML processor. Now we have an object that we can manipulate.
37-38 This block of code generates part of the user interface. Using XSLT, we can transform the request document that we just loaded into an HTML string and display it on the page. In the HTML source, you'll see an HTML DIV element with an ID of SOAPDiv. This line sets the innerHTML property to a string of text. The transformNode method of the SOAPRequest object invokes the XSLT processor, which creates an HTML object. You can view this object on the page by setting the innerHTML property of the DIV.
39 The SOAP spec suggests that it's good practice to include the SOAPMethodName parameter as part of the HTTP request header. The value of this parameter should be the name of the method you are invoking.
40 The Send method sends the packet to the SOAP server. (The SOAP server was opened on line 16.) Remember that the SOAPRequest object is an in-memory, binary XML object. We need to re-serialize (turn the object into a sequential stream of XML characters) using the .xml property. The Send method first creates the HTTP header with the SOAPMethodName parameter, then creates a blank line (which separates the header from the body), and finally concatenates the XML document.
41 Now we wait. Since we set the async property to false, the process will wait on line 40 until the SOAP server returns an HTTP response packet. The packet comes in the form of a plain XML document (a serialized stream of Unicode characters). If we want to access this packet in our script, we must load this stream into the XML DOM as a tree object. The LoadXML method does this.
42 We want to process the return value as an HTML document. We can then return this document to the calling function. In this example, the function sets the innerHTML property of another DIV.

Implementing a SOAP Server in OmniMark

The SOAP server opened on line 16 in Listing 8-5 is an OmniMark program that calls another OmniMark program named SOAPServer.xom. SOAPServer.xom runs as an HTTP server. Let's take a look at OmniMark.

OmniMark is a fourth-generation, event-driven language that runs on most modern computer platforms. The language was first published in the early 1990s as a conversion tool. OmniMark has really grown up since then and is now a viable alternative to Perl for industrial-strength network programming applications.

OmniMark has an easy-to-learn syntax that resembles English. It also has built-in XML parsing and processing capabilities and by far the best pattern-matching processor available anywhere.

OmniMark is free. You'll find a copy on the CD included with this book. The CD also includes a terrific integrated development environment (IDE). This IDE is free for use at home and in school, but it will cost you a few hundred bucks if you want to use it at work. You'll find all this in the \OmniMark directory on the CD.

Since OmniMark is largely self-documenting, it is pretty easy to describe and learn. I've included my book OmniMark at Work, Volume 1: Getting Started on the companion CD. I wrote the book before the release of the current version of OmniMark, so the network programming concepts are fairly limited. However, the book does explain OmniMark's basic architecture, variable types, control structures, and techniques. A more recent description of OmniMark appears in Appendix A.

I want to show you two OmniMark programs here. The first is a CGI program named SOAPServer.xom that we call from our script. This program will parse the SOAP message and determine whether a service that can handle the request is loaded. The second program, SOAP.xin, runs continually as a service. This service listens at a known IP port and is activated when our CGI program sends the service a message on that port. The message sent by the CGI program is an XML stream. The service processes this request, formulates a response, and then sends the result back to the waiting CGI program.

In this example Web service, an XML document is passed between the OmniMark program acting as a SOAP server and the OmniMark process acting as the Web service. However, it is important to note that the programs that provide services in a Web service environment don't need to know anything about XML. The SOAP server's job is to translate the SOAP request into whatever language or form the service object requires. This means that 20-year-old COBOL programs can still work nicely as services. All the SOAP server needs to do is translate the SOAP request into the COBOL program's calling convention and turn the COBOL program's response into a SOAP response document.

SOAPServer.xom

We call the first program, SOAPServer.xom, as a CGI program in the same way we might call Perl programs. You can call the OmniMark program directly through a URL, or you can redirect it from an arguments file, as I have done in this example. The arguments file is SOAPServer.xar. I like this latter approach because it allows me to specify command-line arguments and have the OmniMark program execute in a location other than the scripts directory (perhaps from a hidden path or on another machine). Listing 8-6 shows the argument file.

OmniMark has a rich command-line interface. SOAPServer.xar contains the options passed to this interface. In line 1, the actual server program, SOAPServer.xom, is called as the source file (-sb). The -i directive in line 2 contains a path to common include files, and the -x directive in line 3 contains the path of the compiled libraries. Any errors reported by the program, or those sent to the #error stream, are sent to the file following the -log directive in line 4.

Listing 8-6. An argument file for calling an OmniMark SOAP server.

 

SOAPServer.xar

-sb SOAPServer.xom -i "c:\program files\omnimark\xin\" -x "c:\program files\omnimark\lib\=L.dll" -log SOAPServer.log

Under some operating systems, OmniMark also supports the "hash-bang" directive as the first line in the file, somewhat like Perl does. The hash-bang directive looks like this:

 #!/usr/bin/omnimark/bin/omnimark -sb helloworld.xom 

Listing 8-7 contains the complete SOAPServer.xom CGI program. A line-by-line description of the program follows in Table 8-3.

Listing 8-7. The OmniMark SOAP server as a CGI application.

 

SOAPServer.xom

1 include "SOAP.xin" 2 3 process 4 local stream methodName initial {""} 5 local stream SOAPResponse 6 local stream formVars variable initial-size 0 7 local stream remote-host initial {"localhost"} 8 local TCPConnection myConnection 9 local stream myRequest 10 local stream responseXML 11 12 cgiGetEnv into formVars 13 set methodName to UTIL_GetEnv ("HTTP_SOAPMETHODNAME") 14 15 open SOAPResponse as buffer 16 do scan #main-input 17 match any{formVars{"CONTENT_LENGTH"}} 18 => inputData 19 do when methodName = "" 20 put SOAPResponse "%n<SOAP:Fault>" 21 || "%n <faultcode>101</faultcode>" 22 || "%n <faultstring>no method specified" 23 || "%n</faultstring>" 24 || "%n</SOAP:Fault>" 25 else 26 do when portSOAP has key methodName 27 set myConnection to TCPConnectionOpen 28 on remote-host 29 at portSOAP {methodName} 30 open myRequest 31 as TCPConnectionGetOutput myConnection 32 put myRequest inputData || crlf 33 close myRequest 34 35 do when TCPConnectionIsConnected myConnection 36 set responseXML to 37 TCPConnectionGetCharacters myConnection 38 put SOAPResponse responseXML 39 done 40 else 41 put SOAPResponse "<SOAP:Fault>" 42 || " <faultcode>101</faultcode>" 43 || " <faultstring>method not supported:" 44 || " %g(methodName)</faultstring>" 45 || "</SOAP:Fault>" 46 done 47 done 48 done 49 close SOAPResponse 50 51 output "Content-Type: text/xml" || crlf 52 || "Cache-control: private" 53 || crlf || crlf 54 || "<?xml version='1.0'?>" 55 || "%n<SOAP:Envelope " 56 || "%n xmlns:SOAP='urn:schemas-xmlsoap-org:soap.v1'>" 57 || "%n <SOAP:Body>" 58 || SOAPResponse 59 || "%n </SOAP:Body>%n</SOAP:Envelope>%n" 60 61 catch #external-exception 62 identity catch-id 63 message catch-msg 64 location catch-loc 65 output 'An external exception occurred in SOAPServer.%n' 66 || '%g(catch-loc)%n' 67 || '%g(catch-id) : %g(catch-msg)%n'

Table 8-3. A line-by-line description of the OmniMark SOAP server in Listing 8-7.

Line Description
1 The include directive does what you would expect: includes the code contained in SOAP.xin at this point in the program. SOAP.xin contains a common set of constants and functions used by this program and the Web services that this program calls.
3 OmniMark has two modes. It can act as a rules-based, event-driven environment in which some event (usually in the input stream) fires rules, where actions kick in. In this way, OmniMark resembles a fourth-generation object-oriented programming language. The OmniMark's other mode—called a process program—is like a third-generation language such as C. Our CGI program is this second type (a process program). In this mode, actions will be executed in the order in which they are written.
4-10 OmniMark has four basic types of variables: counter (integer data type), stream (string), switch (Boolean), and pattern. In addition to the four built-in variable types, OmniMark external functions can define their own data types, called opaque data types. We will use opaque data types when we connect to a database and retrieve records from queries.

You must declare all variables except pattern variables as global or local. Global variables are available anywhere in the program. Local variables are available to any actions that are in the scope where you defined the local variable. (For more information about declaring variables, check out Chapter 5 of OmniMark At Work, available on the companion CD.) We'll use a debug stream to contain information that we might need if the program doesn't work exactly right.

We must declare variables as the first elements inside of a scope. In this example, the scope is the process program. These variables will be active throughout the entire process program. If a local variable of the same name is declared in a subordinate scope, the inner local variable will override the outer local variable for the duration of the inner scope. The outer variable will take over once the scope in which the inner variable is declared ends.

Welcome to hierarchical programming!

4 The methodName variable will contain the name of the method we are invoking.
6 Any OmniMark variable can exist as a "shelf." A shelf is a collection of like objects that can be accessed by their index offset (item 1, item 2, and so on) or by a descriptive key. A shelf is similar to an associative array in Perl.

Here, we are creating a collection of streams under the name formVars. The keyword variable indicates that this collection is a shelf of like items, rather than a single variable.

8 The TCPConnection opaque variable type is defined by one of the external libraries included in SOAP.xin. We create a local name, myConnection, which will act as our handle to make TCP connections.
12 The cgiGetEnv function retrieves the values of all CGI-related environment variables. The function places those values on a keyed stream shelf of name-value pairs, in which the key of the item is the name of the environment variable and the item's value is the value of that variable. The formVars shelf now contains a collection of streams that are specified with associated keys. You can see some of these server variables in Table 8-4.

To access any of these values, you need to indicate the member of the collection. For example, to access the QUERY_STRING, the OmniMark code is:

 set queryString to cgiGetEnv["QUERY_STRING"] 

13 The cgiGetEnv function gets only predefined and expected server variables. HTTP 1.1 allows us to create our own environmental variables. The environmental variable we use for SOAP is the SOAPMethodName parameter. The UTIL_GetEnv function accesses any parameter from the HTTP header. To get the parameter we want, we need to add HTTP_ to the beginning of the parameter's name and convert everything to uppercase.
15 You can use the set action to set a stream to some value, or you can open, append, and close a stream just as you would a file. In this example, we want to open the SOAPResponse stream in the same way we'd open an in-memory file so that we can occasionally put data into it.
16 The OmniMark CGI interface exposes the contents of the POST method as coming from the data source #main-input, which is a read-only stream.

The do scan control structure exposes a string to a series of pattern- matching statements (match). If any of these patterns are found, the actions following the match statement are executed.

17 This line matches all the posted text. The any pattern declaration by itself will find a single character. If any is followed by an integer in curly brackets, it will find the number of characters indicated by that integer. In this case, we are interrogating the CONTENT_LENGTH environment variable sent in the HTTP header and grabbing exactly that number of characters into a pattern variable called inputData.
19 The do when control block is like the if statement in most languages. In this case, if the methodName isn't set, we need to indicate an error because we can't figure out what method is requested.
20-24 SOAP defines an element for indicating that something went wrong with the SOAP request. The faultcode and faultstring elements are set to codes that might help the SOAP client make sense of the problem.

The OmniMark concatenation characters|| will concatenate the string into a single unit. I use this to make the program more readable. The "%n" string inserts a newline character.

26 The portSOAP variable is a keyed array of items that was initialized in SOAP.xin. This array contains the names of the services offered, along with the port at which each service listens. The declaration looks like this:

 global stream portSOAP variable initial { "5432" with key "getWeather", "5433" with key "getXMLNewsRequest", "5434" with key "getAtomTimet"} 

If this stream shelf has an item with the key equal to the methodName of the request, we know to send the package to the port indicated in that member of the shelf.

27-31 Now we need to establish a connection to the port so that we can communicate with the service.
32 Once we've established the connection and opened the port, the put action sends data over the line. In this case, the OmniMark object is aware of XML and SOAP, so we send the entire SOAP document. However, we could use this same technique if the object knew nothing about XML or SOAP. The SOAP server's job is to make the translation between the SOAP request that comes in and whatever the object needs to service the request.

At this point, the service that is listening at the port indicated will take over. Listing 8-9 shows the program, getXMLNews.xom. It will receive the package, do its magic, and then return a package to the server. In this case, it the getXMLNews program will give back an XML document that contains the data generated by the service.

35 A loop checks to assure that the connection is still active.
36-37 The server waits at this point until characters are sent back from the service. As characters are returned, the responseXML streams are captured.
40-45 If we don't recognize the methodName, we need to generate a SOAP:Fault package with useful information.
49 Like a file stream, an OmniMark stream must be closed before it is read back in. At this point, the SOAPResponse stream has the payload that we need to send back to the client.
51-59 The output action will send something to the output stream. This is a tricky concept in a hierarchical programming language. OmniMark allows you to redirect the output in many different ways. In this example, the output stream is the client that invoked the CGI program.
51-52 We need to set some HTTP header variables.
53 Two newline characters in a row separates the HTTP header from the body.

Because we are creating an XML document, good form dictates that we include the XML declaration. The SOAP:Envelope root element follows. The two vertical bars indicate string concatenation.

61-67 OmniMark has a rich exception-handling interface. Actions in this catch directive will be executed in the event of a critical program error. Like virtually everything else in OmniMark, exception handling is, hierarchical in nature, allowing one nested routine to throw an exception to an outer routine.

Table 8-4. Server variables accessed by the cgiGetEnv command.

Variable Description
AUTH_TYPE The authentication protocol currently being used. This variable is defined only if the server supports—and if access to the CGI program requires—authentication.
CONTENT_LENGTH The length, in bytes, of the information the Web server sends to the CGI program as input. You'll use this variable most often when the CGI program uses the POST method to process input sent from an HTML form.
CONTENT_TYPE The type of content the Web server sends to the CGI program as input.
HTTP_CONNECTION The type of connection that the client and server use. For example, HTTP_CONNECTION = Keep-Alive.
HTTP_HOST The IP address or host name of the accessed machine.
HTTP_USER_AGENT The browser software and operating system that the client system is running.
QUERY_STRING Contains the encoded data from a form submission when that form is submitted by the client by using the GET method. If a form is submitted using the POST method, this environment variable is not set, as the encoded data is passed to the CGI program through standard input (in OmniMark terms, through #process-input).

getXMLNews.xom

Listings 8-8 and 8-9 show the service that waits for the getXMLNewsRequest method. The file is named getXMLNews.xom. Listing 8-8 shows the server loop, which listens to the appropriate port, reads data over TCP, and calls the function that provides the service. Table 8-5 breaks down this listing line by line. Listing 8-9 shows the service provider: a function that opens the database, retrieves the records, and builds the XML response payload. Table 8-6 offers a line-by-line description of Listing 8-9.

Listing 8-8. The service that listens for getXMLNewsRequest methods.

 

getXMLNews.xom

1 include "SOAP.xin" 2 declare catch server-die 3 4 process 5 local TCPService myService 6 local TCPConnection myConnection 7 local stream caughtData 8 local stream responseXML 9 10 set myService to TCPServiceOpen 11 at portSOAP{"getXMLNewsRequest"} 12 put #error "getXMLNews listening on " 13 || portSOAP{"getXMLNewsRequest"} ||"%n" 14 throw server-die 15 unless TCPService-is-working myService 16 17 repeat 18 set daysWanted to 0 19 20 set myConnection 21 to TCPServiceAcceptConnection myService 22 throw server-die 23 unless TCPService-is-working myService 24 set caughtData to 25 TCPConnectionGetCharacters myConnection 26 27 put #error "getXMLNews caught and processed data on " 28 || portSOAP{"getXMLNewsRequest"} 29 || "%n----%n%g(caughtData)%n----%n" 30 31 clear fieldsWanted 32 using group processXML do 33 do xml-parse instance scan caughtData 34 suppress 35 done 36 done 37 38 open responseXML as TCPConnectionGetOutput myConnection 39 put responseXML getXMLNews(daysWanted, formatType, 40 fieldsWanted) || crlf 41 close responseXML 42 catch #program-error 43 put #error "caught #program-error%n" 44 45 catch #external-exception 46 identity catch-id 47 message catch-msg 48 location catch-loc 49 output 'An external exception occurred.%n' 50 output '%g(catch-loc)%n' 51 output '%g(catch-id) : %g(catch-msg)%n' 52 again 53 54 catch server-die 55 open responseXML as 56 TCPConnectionGetOutput myConnection 57 put responseXML "<msg>getXMLNews Server killed</msg>" 58 || crlf 59 close responseXML 60 halt 61 ;-------------------------------------------- 62 group processXML 63 ;-------------------------------------------- 64 element #implied 65 suppress 66 67 element format 68 set formatType to "%c" 69 70 element days 71 set daysWanted to "%c" 72 73 element fields 74 repeat scan "%c" 75 match [any except " "]+ => field 76 set new fieldsWanted key field to field 77 unless fieldsWanted has key field 78 match any 79 again 80 81 element die 82 do scan "%c" 83 match content-start POISON-PILL content-end 84 put #error "Shutting Down%13#%10#" 85 throw server-die 86 done

Table 8-5. Line-by-line description of the service that listens for getXMLNewsRequest methods in Listing 8-8.

Line Description
1 The include directive does what you would expect: includes the code contained in SOAP.xin at this point in the program. SOAP.xin contains a common set of constants and functions used by this file and the Web services that this program calls.
2 OmniMark's catch and throw capabilities give you the ability to declare your own rules that catch packages thrown under program control or as the result of an exceptional situation. In this case, we declare a catch that will be accessed if someone sends a poison pill to the server to shut it down.
4 We are running another process program.
5-6 We need to declare two variables for the TCP service and connection. These variables will be the conduit through which we communicate with the SOAP server.
10-11 The service is opened at the port indicated by the stream declared in SOAP.xin.
12-13 The #error stream is a write-only stream that is usually the command-line console from which the program was executed. This message will be displayed as the program starts.
14-15 If the TCP service has a problem, an exception is thrown to a catch that is defined elsewhere.
17 Unlike the CGI program you saw earlier in the chapter, this program stays in memory until it is killed by a poison pill or something grave happens with the TCP service manager. The repeat…again control structure is the loop that will be executed once for each message the program receives.
20-21 Each time through the loop, we need to establish a connection with the TCP service.
22-23 We need to check that the TCP service is healthy. If not, an exception is thrown.
24-25 This section of the loop is where the server spends most of its time waiting. The TCPConnectionGetCharacters function will catch messages sent to this program by the CGI program SOAPServer.xom.
27-29 Display the package on the console, just to provide some feedback for anyone who might be monitoring it.
31 The SOAP package indicates which fields the service requests. A shelf of streams contains those fields. Since we are in a server situation, we expect this program to be up and running for many, many requests. Therefore, we need to make sure we clean up after ourselves. By clearing this shelf, we set the number of items to zero so that later they can be initialized by another routine.
32-36 OmniMark has XML processing available natively. Because we received an XML document, we need to look into that document to find out the parameters for our process. A group called processXML is elsewhere in the program. That group contains a set of rules that will process XML elements as they are encountered by some requesting process. This code processes the XML document in caughtData by invoking the XML parser and exposing the data to the element rules. Let's take a look at that now.
62 The group declaration creates a programming scope that we can call by name. This group contains a collection of element rules that are fired when the parser encounters certain elements in the input stream. The input stream, in this case, is the data sent here with the do xml-parse action on line 33.

The XML document we are processing looks like this:

      <SOAP:Envelope xmlns:SOAP="urn:schemas-xmlsoap-org:soap.v1"> <SOAP:Body> <getXMLNewsRequest xmlns="urn:schemas-architag-com:getXMLNews"> <days>10</days> <format>XML</format> <fields>headline url abstract</fields> </getXMLNewsRequest> </SOAP:Body> </SOAP:Envelope> 

64-65 The #implied element rule will fire if no other element rule exists for a given event. This element rule will process all the elements in the SOAP namespace because we don't care about them at this point. All we really want are the number of days, the appropriate format for outputting, and the fields the user wants.
67-71 When the format element comes along, this element rule will fire, setting the format type to the appropriate value. The "%c" indicates the content of the element. Every element rule must process the contents exactly once with either the "%c" or suppress. (Note that the getXMLNews.xom program does not support any format type except raw XML.)

The days element rule captures the days element and sets the appropriate variable.

73 This element rule will fire when the fields element comes along.
74-79 The repeat scan control structure is similar to do scan, except it repeatedly processes the string until a character comes along that is not matched. The pattern following each match statement is evaluated to see whether it matches what is currently being scanned. As soon as a match is found in the loop, the actions underneath are processed and the process continues with the next character to be scanned.

In this case, the contents are being scanned to see what fields are wanted. When a field is found, a new fieldsWanted item is created on the shelf.

81 This server program will run forever. You might need to kill the server for some reason. To kill the server, send the following code as one of the elements inside the SOAP request:

      <die>poison-pill string</die> 

This string is set in SOAP.xin. I recommend that you keep the string private, since anyone who can create a request package for your server can kill that request if he knows the poison pill string.

82-83 The poison pill string must be the only string inside the die element. Use the positional patterns, content-start and content-end, to achieve this.
84-85 If this string is found, an error will be sent to the console, and an exception thrown to the server-die catch,…
54-59 …which you'll find right here. This routine sends a single msg element, closes the connection, and exits the program gracefully.
38 Once we have all the data points we need, we can send the data back to the client, which should still be waiting for a response. The responseXML stream is used to establish a response stream. All we need to do is open the output connection, put data into it just as if it was a normal stream, and then close it. When the stream is closed, the data is sent back.
39-40 The getXMLNews function is described later. This function returns an XML document that contains the news, depending on the parameters selected.
42-43 OmniMark has rich exception-handling capabilities. Line 42 will catch any hard program errors. (These are run-time errors, such as those that result when trying to put data into an unopened stream or when accessing the value of an open stream.)
45-51 The exception handler in line 45 will catch external errors, such as trying to access a database that isn't open or trying to write to a read-only file.
52 This is the end of the repeat loop started in line 17.

Listing 8-9. The OmniMark function that returns an XML document that will become the SOAP payload.

 

getXMLNews return # function

1 macro outField token tagName is 2 do when fields has key tagName 3 put newsXML "%n <%@(tagName)>" 4 || cleanString 5 (dbFieldValue rsXMLNews key tagName) 6 || "</%@(tagName)>" 7 done 8 macro-end 9 10 define stream function getXMLNews ( 11 value counter daysDesired, 12 value stream format, 13 read-only stream fields) as 14 local stream newsXML initial {""} 15 local dbDatabase dbXMLNews 16 local dbField rsXMLNews variable 17 local stream strSQL 18 local stream minusDate 19 local stream finalDate 20 21 set dbXMLNews to dbOpenODBC "XMLNews" 22 set minusDate to add-to-ymdhms now-as-ymdhms days -daysDesired 23 set finalDate to format-ymdhms "=xM/=xD/=xY" with date minusDate 24 set strSQL to "SELECT * FROM News " 25 || "WHERE Date >= #%g(finalDate)# ORDER BY Date DESC" 26 dbQuery dbXMLNews sql strSQL record rsXMLNews 27 28 open newsXML as buffer 29 do when dbRecordExists rsXMLNews 30 put newsXML "%n<getXMLNewsResponse days='%d(daysWanted)'>" 31 repeat 32 exit unless dbRecordExists rsXMLNews 33 put newsXML "%n <item date='" 34 || dbFieldValue rsXMLNews key "date" || "'>" 35 36 outField "url" 37 outField "location" 38 outField "headline" 39 outField "abstract" 40 41 put newsXML "%n </item>" 42 dbRecordMove rsXMLNews 43 again 44 45 put newsXML "%n</getXMLNewsResponse>" 46 else 47 put newsXML "%n<getXMLNewsResponse days='%d(daysWanted)'/>" 48 done 49 50 close newsXML 51 return newsXML 52 53 catch #external-exception 54 identity catch-id 55 message catch-msg 56 location catch-loc 57 output 'An external exception occurred.%n' 58 output '%g(catch-loc)%n' 59 output '%g(catch-id) : %g(catch-msg)%n'

Table 8-6. Line-by-line description of server program in Listing 8-9.

Line Description
10-13 OmniMark has two ways of creating callable sets of code. These lines contain a function definition. You can call functions from just about anywhere in an OmniMark program. This function, getXMLNews, returns a stream variable. The function is called with three arguments: a counter, a stream, and a shelf of streams.
14 The newsXML stream is where we build the XML document that we return as the SOAP payload.
15-16 The OmniMark Database library (OMDB) provides a set of objects and functions that simplify connecting to databases. Here we create two database objects: one for the ODBC database connection and one for a recordset that will be returned as the result of a SQL query.
21 The dbOpenODBC function opens an ODBC connection with the name XMLNews and returns a handle.
22-23 To build the SQL query, we need to calculate a date that is equal to a number of days prior to the current date. This offset is sent to us in the daysDesired argument to this function. The OmniMark date-time library has a number of functions that allow date mathematics. All dates are stored in a year-month-day-hour-minute-second format called ymdhms. First we determine the desired start day by subtracting daysWanted from the current day. Then we format that date into a form acceptable to the SQL processor. In this case, the date is in the form 4/26/2000.
24-25 We build the SQL query by plugging in the date calculated above. Then we send the query to the connected database with the dbQuery function. A recordset will be returned into rsXMLNews. This recordset is exposed as a shelf of an opaque data type defined by the OMDB functions.
28 The newsXML stream is opened as a file-like buffer so that we can append to it.
29 The dbRecordExists function checks to see if the SQL query returned any records.
30 If the query did return any records, we need to start building the XML document with a root element of getXMLNewsResponse.
31-43 This repeat…again loop will execute once for each record returned from the query.
32 Once the last record is processed, the dbRecordExists function will return a false, at which time the repeat…again loop is exited.
34 We access the fields in the recordset object the same way we access keyed shelves—that is, we access their value by finding the item that has a certain key value. We use the dbFieldValue function to expose this information. This line will output the value of the field named date that was returned from the SQL recordset.
36-39 The outField macro will be called four times, passing the names of fields in the database.
1-8 Macros are simple yet powerful. A macro is really just a fancy string-replacement process done at pre-compile time. This macro will check to see whether the field indicated is one of the fields we asked for. If it is, the macro will create an element and put the value of the field inside.
42 The dbRecordMove function loads the next record into the rsXMLNews record set.
45 This line ends the getXMLNewsResponse document.
47 If no records return from the SQL query, we create an empty element, just so that we'll know nothing is there.
50-51 Close the stream and return it to the calling application.
53-59 This catch will execute if there is a problem with some external interface, usually caused by a file not found somewhere or some database trouble.

Testing the Service

Follow these steps to get our entire example running:

  1. Start the getXMLNews service with the following command line:
  2.  omnimark -sb getXMLNews.xom -i "c:\omnimark\xin\" -l "c:\omnimark\lib\=L.dll" -log getXMLNews.log 

    The paths for the libraries (-l) and include files (-i) will vary depending on where you installed OmniMark.

  3. Load the HTML driver program, getXMLNews.htm, into your Web browser.
  4. Enter a value for the number of days, the format (remember that raw XML is the only format supported by this version), and the fields you want to see. Click Get News and watch the console run the service, as shown in Figure 8-5.
  5. click to view at full size.

    Figure 8-5. The console showing output from getXMLNews.xom.



XML and SOAP Programming for BizTalk Servers
XML and SOAP Programming for BizTalk(TM) Servers (DV-MPS Programming)
ISBN: 0735611262
EAN: 2147483647
Year: 2000
Pages: 150

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