Web Server Application Tutorial
In this section we will try to build a moderately sophisticated example that uses a design that will allow the
program to run using all of the various web server deployment strategies. As our example, we will use the
sbisreportfast.sma program provided in the Projects
directory. The
program starts with the function main()
as shown below.
function main(cgicall cgi) string sReturnval ContactFile cf string sISAPIPhysPath sISAPIPhysPath = cgi.getvariable("APPL_PHYSICAL_PATH") sISAPIPhysPath = .if(sISAPIPhysPath > "", \ rtrim(sISAPIPhysPath, "{0}"), "") cf =@ init(sISAPIPhysPath) sReturnval = fcgi(cgi, cf) fcgiterm() end function sReturnval
The interesting thing to note in this function is that there is very little to the function itself. The
function calls the init()
function, then passes the return value from that function
to the fcgi()
function and receives the return value from that and finally calls the
fcgiterm()
before returning the return value from the fcgi
function. The reason for this design is that although both ISAPI and CGI programs (like almost all
SIMPOL programs) begin with the main()
function, Fast-CGI programs are initialized
using the fcgiinit()
function, subsequent calls only call the fcgi()
function, and when the Fast-CGI instance is closed, only then will the fcgiterm()
function be called. To write for all three architectures requires a little bit of planning, so since the
Fast-CGI version would never call the main()
function, everything is designed for
the Fast-CGI version and the other two use the main()
function to call the Fast-CGI
components. In the case above, since the init()
function is used in several places in
the contact system, it was decided to have both the main()
and
fcgiinit()
functions call a common function which in both cases returns what is then
required.
Taking a closer look at the beginning of the program another ISAPI-specific item can be seen. That is the
request for the variable APPL_PHYSICAL_PATH
. This variable is only available in ISAPI
(and possibly only in Microsoft Internet Information Server (IIS). There are a number of ISAPI-specific
variables that can be retrieved, see the IIS documentation for details. The reason that this is so
important is that unlike when using CGI or Fast-CGI, ISAPI is done via a Dynamically Linked Library
or DLL. DLL's don't have a concept of a current directory when they are executing, so they always inherit
the current directory of the parent process, in this case that of the web server. That may or may not be
important depending on your web server application, but as you will see later, in this case, knowing the
current directory or more importantly the location where the web server application was loaded from is
important.
The next thing to note about the main()
function is the use of a user-defined type
called ContactFile. This type was automatically created using an SBL program and in this case
is included using the include directive and compiled into the program. In other cases, it
may be copmiled as a standalone library. It is a type that wraps up a Superbase file that is hosted using
PPCS. In the near future a utility program will be created in SIMPOL that produces this from an
SBD
or by interrogating a PPCS-based table. Using this automatically
generated type, it is much easier to access the various parts of the CONTACT
database
table used by the sample contact system. Below is the code that makes up the type:
//-----------------------------------------------// // CONTACT // // Constants and Type definitions // //-----------------------------------------------// constant fCONTACTNAME "CONTACT" constant CONTACT_LASTNAME "LastName" constant CONTACT_FIRSTNAME "FirstName" constant CONTACT_CONTACTNO "ContactNo" constant CONTACT_PHONE "Phone" constant CONTACT_FAX "Fax" constant CONTACT_ADDRESS "Address" constant CONTACT_CITY "City" constant CONTACT_STATE "State" constant CONTACT_ZIP "ZIP" type ContactFile export ppcstype1file file ppcstype1field sLastName ppcstype1field sFirstName ppcstype1field sContactNo ppcstype1field sPhone ppcstype1field sFax ppcstype1field sAddress ppcstype1field sCity ppcstype1field sState ppcstype1field sZIP function open end type function ContactFile.open(ContactFile me, ppcstype1 ppcs, \ string sIpaddress) ppcstype1file f integer iErrnum iErrnum = 0 f =@ ppcs.openudpfile(sIpaddress, fCONTACTNAME, error=iErrnum) if f !@= .nul me.file =@ f me.sLastName =@ getfield(f, CONTACT_LASTNAME) me.sFirstName =@ getfield(f, CONTACT_FIRSTNAME) me.sContactNo =@ getfield(f, CONTACT_CONTACTNO) me.sPhone =@ getfield(f, CONTACT_PHONE) me.sFax =@ getfield(f, CONTACT_FAX) me.sAddress =@ getfield(f, CONTACT_ADDRESS) me.sCity =@ getfield(f, CONTACT_CITY) me.sState =@ getfield(f, CONTACT_STATE) me.sZIP =@ getfield(f, CONTACT_ZIP) end if end function iErrnum
Our next step is to have a look at the init()
function. It is shown below:
function init(string sISAPIPhysPath="") ppcstype1 ppcs string sIpaddress integer iErrnum ContactFile cf cf =@ ContactFile.new() iErrnum = 0 ppcs =@ ppcstype1.new(udpport=.nul, error=iErrnum, \ username="sbiscontact") sIpaddress = "" getprivateprofilestring(sCGISECTION, sCGIPPCSSERVER, \ sDEFIPADDRESS, sIpaddress, \ sISAPIPhysPath + sCGIINIFILE, \ sCGIINIEOLCHAR) if sIpaddress > "" if cf.open(ppcs, sIpaddress) != 0 cf =@ .nul end if end if end function cf
As we can see from the program code, the primary purpose of this function is to create an object of type
ContactFile, create a ppcstype1 object, and then using these two objects and the
IP address that is retrieved from a configuration file, to open the CONTACT
database
file. In a complex example this function might be opening dozens of database files for use in a web server
application. Earlier we discussed the need to retrieve the physical path to the SIMPOL program in an
ISAPI environment. The reason is the code in this function that reads a setting from a configuration
file. It would not be a very good design to hard code the IP address and port of the PPCS server, since
moving the server would require recompiling the code each time. It is more effective to put these kinds of
settings in a configuration file and retrieve them at runtime. The implementation of the function
getprivateprofilestring()
is reasonably compatible with the Windows function of the
same name, minus a few limitations and the fact that it works on multiple platforms. It can be found in the
conflib.sml
library in the lib
directory.
The actual fcgi()
contains little more than a call to the actual function that does
the work, as can be seen below:
function fcgi(cgicall cgi, ContactFile cf) SBISReportFast(cgi, cf) end function ""
The basic design of a typical web server application uses a sandwich approach. The top of the page is one
slice of bread, the bottom of the page is the other, and the output from the program is the filling. If the
application is designed carefully making use of cascading style sheets, then changing the look and feel of
the web site can be done without even recompiling the program. The way that is done is to use the
HTML_Include()
function to output the top and bottom from files that are located in
the directory from where the program is loaded or some other consistent location. This function is part of
the sbislib.sml
.
function SBISReportFast(cgicall cgi, ContactFile cf) integer iErrnum string sTmp, sDateFormat, sTmp2, sTmp3 ppcstype1record r date dt objset obsBase objsetelementref n SBLlocaledateinfo ldiLocale string sISAPIPhysPath datetime dtStart, dtEnd boolean bFound sISAPIPhysPath = cgi.getvariable("APPL_PHYSICAL_PATH") sISAPIPhysPath = .if(sISAPIPhysPath > "", \ rtrim(sISAPIPhysPath, "{0}"), "") ldiLocale =@ SBLlocaledateinfo.new() sDateFormat = "mmmm dd, yyyy" iErrnum = 0 sTmp = "" dtStart =@ datetime.new() dtEnd =@ datetime.new()
In the initial segment of the SBISReportFast()
the initialization is done. The function
makes use of a number of types and functions provided by libraries that are written and compiled in SIMPOL
itself. These types include the objset, objsetelementref, and the
SBLlocaledateinfo. The first two types are part of the objset.sml
library,
which provides a set object that operates very similarly to the set object in SBL but which has a key value
that must be a string and an optional object reference. This allows the collection and sorting by key value
of a set of objects of any type. In this example we will use it for storing the output string in order by
a three-level sort. The last of the types is part of the implementation of date format functions to be found
in the SBLDateLib.sml
file. In SBL there are certain global values that determine the
formatting for dates, including the names of the days of the week, the months of the year, and the abbreviated
months of the year. Since there is nothing global in SIMPOL, this needs to be handled differently. In this
program we initialize an object of type SBLlocaledateinfo and then pass it to the functions that
require this object. This particular library is compatible with functions found in SBL, so there are no
options that would not exist in SBL. There are other libraries being built that provide more sophisticated
date formatting routines, though the SBL-compatible ones should be used when working with data from tables
via the PPCS type 1 protocol.
The next section of the program checks to see if the return value from the init()
function actually contains an object or if it failed (returned .nul
). If it succeeded, it
then outputs the content type. Unlike the older Superbase Internet Server product (SBIS), web server
programs in SIMPOL can work with any content type desired or required. After outputting the content type
and the header, the top of the sandwich is loaded and output by the HTML_Include
function. Finally, the table is set up and the header is output including the current date.
if cf =@= .nul or cf.file =@= .nul CGIFileError(cgi, .nul, "Error opening database \ file 'CONTACT'") else cgi.output("Content-type: text/HTML{d}{a}{d}{a}", 1) cgi.output("<html><head><title>" + sTITLE + \ "</title>" + CRLF, 1) /////////////////////////////////////// // External HTML File // // Include the header and css // /////////////////////////////////////// HTML_Include(cgi, sISAPIPhysPath + "header.htm") ////////////////////////////////////// // Program title // ////////////////////////////////////// //cgi.output( CRLF, 1) cgi.output('<tr><td><center><h2 class="titledblue">' + \ sTITLE + '</h2></center></td>'+ CRLF, 1) cgi.output('</tr>'+ CRLF, 1) ///////////////////////////////// // Center the table // ///////////////////////////////// dt =@ date.new() dt.setnow() sTmp = DATESTR(dt, sDateFormat, ldiLocale) cgi.output('<tr><td align="center"><center><h3 \ class="titledblue3">' + sTmp + ' - Partial \ Client Quick Listing</h3></center></td></tr>\ <tr><td><center><span class="textitalic">Where \ the first letter of the last name is equal to \ ''D'' and the result is sorted by <strong>City\ </strong>, then <strong>LastName</strong>, \ then <strong>FirstName</strong>.</span>\ </center><br></td></tr>'+ CRLF, 1) cgi.output('<tr><td><center><table border=1 width="510">' + \ CRLF, 1) cgi.output('<tr>' + CRLF, 1) cgi.output('<th class="stdhdr" width="25%">City</th>' + \ CRLF, 1) cgi.output('<th class="stdhdr" width="25%">Last name</th>' + \ CRLF, 1) cgi.output('<th class="stdhdr" width="25%">First name</th>' + \ CRLF, 1) cgi.output('<th class="stdhdr" width="25%">Telephone</th>\ </tr>' + CRLF, 1)
Once the preparations are complete, the main part of the program can begin. This is the part of the program that reads the records that match the search criteria, formats the output, and then outputs the result. In this case the program begins by recording the starting time for the search. It then selects the first record in the table according to the LastName index that begins with the letter "D". To make the selection the ContactFile object is used. Each of the properties corresponds to a field in the file with the same name (fields with spaces in the name have the spaces converted to underscores). Also, each field is prepended with a single letter that indicates the data type of the field. Fields that are indexed will have an object associated with their index property and using that the first selection can be made. Afterwards, the record object is used to select the next record in the same index order with which the record itself was selected. As each record is selected and determined to be a valid part of the result set, a string is formulated to hold the three-level sort key, first using the name of the city, then the last name, and finally the first name. Following that another string is assigned the components of the final output, which equates to a row of the HTML table. That string is then added as an object to the objset using the first string as the sort key.
Note | |
---|---|
The string is actually passed to the |
This continues until the first non-matching record is found in the index. At that point, the loop exits and the objset contains all of the results in the desired sort order.
// Record the starting time for the search dtStart.setnow() // Select the first record that starts with a D in the // Lastname index and then continue to select records // until the first letter is no longer a D. bFound = .false r =@ cf.sLastName.index.selectkey("D", error=iErrnum, \ found=bFound) obsBase =@ objset.new() //SELECT ; //WHERE Lastname.CONTACT LIKE "D*" //ORDER City.CONTACT,LastName.CONTACT,FirstName.CONTACT //TO APPEND //END SELECT // This section performs the actual report and stores // the sort key plus the desired output into the set. // The set will automatically be stored in sorted order. while r !@= .nul and .lcase(.lstr(r.get(cf.sLastName),\ 1)) == "d" sTmp = PAD(r.get(cf.sCity), 40) + \ PAD(r.get(cf.sLastName), 60) + \ r.get(cf.sFirstName) sTmp2 = '<tr><td class="stdtext">' + \ .lstr(r.get(cf.sCity),15) + \ '</td><td class="stdtext"><a href="' + \ HTML_Page("sbiscontactdisplay.smp", cgi) + \ '?cno=' + r.get(cf.sContactNo) + '">' + \ .lstr(r.get(cf.sLastName),15) + \ '</a></td><td class="stdtext">' + \ .lstr(r.get(cf.sFirstName),15) + \ '</td><td class="stdtext">' + \ .lstr(r.get(cf.sPhone),10) + '</td></tr>' + CRLF obsBase.addelement(sTmp, string.new(sTmp2)) r =@ r.select(.false, error=iErrnum) end while iErrnum > 0
Finishing the report is now just a matter of retrieving the first element of the objset, outputting the element,
and then retrieving the next element until we run out of elements. There is no real need to find out how many
elements there are, since we can just continue until either the returned element is equal to a reference to
.nul
or the t
property is equal to a reference to .nul
.
Then we retrieve the time again and output the rest of the the table and close up the remaining bits of the
HTML.
// This part now outputs the results of the report that // had been stored in the set while gathering the // results. The output comes out in the correct order // sorted three levels deep, by using a combined key // composed of the City, the lastname and the first name // where each of the first two have been padded to 60 // characters wide. n =@ obsBase.getfirst() while n !@= .nul and n.t !@= .nul cgi.output(n.t.element, 1) n =@ n.t.getnext() end while dtEnd.setnow() // After report section cgi.output('</table>' + CRLF, 1) cgi.output('<p><strong>Total: ' + \ .tostr(obsBase.totalcount, 10) + " match" + \ .if(obsBase.totalcount <> 1,"es","") + \ ' found from a total of ' + \ .tostr(cf.file.recordcount(error=iErrnum), 10)\ + ' records. The total search time was ' + \ .tostr((dtEnd - dtStart)/1000000, 10) + \ ' seconds.</strong>\ </p><br></center></td></tr>' + CRLF, 1) //////////////////////// /////////////////////////////////////// // Include external HTML file // // as the footer // /////////////////////////////////////// HTML_Include(cgi, sISAPIPhysPath + "footer.htm") end if end function ""
The very end of the program occurs when the fcgiterm()
is called either from the
main()
function or directly by the Fast-CGI support. In this case, there is nothing
for the function to do, so it is empty.