SIMPOL Documentation

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]Note

The string is actually passed to the addelement() method of the objset by creating a new string using string.new(sTmp2). The reason for this is the objset stores a reference to a string object, not a string itself. If only the sTmp2 string had been passed, each time it goes around the loop a reference to the exact same string object using a different key would be assigned to the element of the objset so that at the end, all of the elements would point to only one string that contained the last value created. To avoid this, a new string object is created and initialized with the value of the string in sTmp2. This ensures that each element references a different string. At that point, the objects are only anchored by the objset, so once the objset goes out of scope, all of the strings are also freed.

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.