Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Suppose you have a database with products. Advertisement on paper is expensive (tell me about it). But advertising on the web is something new and can be relatively cheap. It would be a good start to have your database published on the web, wouldn't it. But setting up your own Site with an NT Web Server running with a tool like IntraBuilder or WebHub will cost you money as well, including time and experience to set it up. This chapter will offer a quick and dirty way to publish the information in your databases on the web: just generate dynamic HTML pages (as opposed to static HTML-pages), based on the record in the tables. Expensive? No. Complex? I don't think so. Let's take a closer look and end up with a little "search engine" for the TDMBKS (The Delphi Magazine Review Database of Delphi Books) on the web, including the individual reviews themselves...
Delphi and CGI
Where HTML is the standard for the (hyper)text format within the documents, CGI stands for Common Gateway Interface, and stands for the standard communication interface between the Client (Web Browser) and the Server (Web Server).
There are at least two different forms of CGI, the plain 'standard' CGI and a higher level called WinCGI (for Windows (NT) CGI).
The first uses environment variables and the standard input and output files, the latter a Windows INI-format file (that specifies the names for the input and output files) to communicate between the client at the Web Browser and the Web Server (running the CGI application).
Delphi 2 CGI applications are non-visual applications, i.e. CONSOLE applications, where the input contains the information (request) send by the client, and the output is the dynamic HTML-document that is generated on the fly (and send back to the client with the Web Browser).
The information that is entered by the client, and sent to the CGI application to be used when generating the HTML page can be sent in two ways: either using environment variables and the standard input file (standard CGI) or by using a Windows INI-file and a specified filename (WinCGI).
This chapter will focus on standard CGI applications only!
CGI Forms
But before we can determine what the client wants, let's first take a look at how the client side actually looks like.
How can a static HTML-page even cause information to be sent over to the Web Server and an CGI application at the server to be executed? For this, we need to use a special HTML-extension called FORMS.
Just like Delphi Forms, a form is a place where Windows Controls (such as an edit box, listbox, combobox, button or multi-line text field) can be used.
Unlike Delphi, we have to non-visually design our forms by writing HTML-code.
Let's take a look at a part of the DELBOOKS.HTM file that is used for the example.
For the complete file, I have to refer to the latest version on-line at http://www.drbob42.com/reviews/tdmbks.htm.
<FORM ACTION="http://www.ebob42.com/cgi-bin/delbooks.exe" METHOD="POST"> <UL> <INPUT TYPE="radio" NAME="DELPHI" VALUE="0" CHECKED>Delphi 1.0x or 2.0x<BR> <INPUT TYPE="radio" NAME="DELPHI" VALUE="1">Delphi 1.0x only<BR> <INPUT TYPE="radio" NAME="DELPHI" VALUE="2">Delphi 2.0x only <P> <LI>Level: <BR><SELECT NAME="Level"> <OPTION VALUE=""> don't care <OPTION VALUE="1"> Beginning <OPTION VALUE="2"> Intermediate <OPTION VALUE="3"> Advanced </SELECT> <P> </UL> <HR> <P> <INPUT TYPE="RESET" VALUE="Reset Query"> <INPUT TYPE="SUBMIT" VALUE="Get Results"> </FORM>The code in the form code I've shown here displays two type of controls: three radio buttons (with choices between "Delphi 1.0x or 2.0x", "Delphi 1.0x only" and "Delphi 2.0x only"), and a combobox with four items (resp. "don't care", "Beginning", "Intermediate" and "Advanced"). There are also two buttons on the form, one of type "RESET", to reset the information we've just entered, and one of type "SUBMIT", to - indeed - submit the information we've just entered. So this is the action a client at a Web Browsers needs to do to send information to the Web Server: click on the SUBMIT-type button (in this case with the text "Get Results" on it). But how does the Web Server know which CGI application to start for the data that is sent over? for that we need to take a look at the ACTION parameter of the FORM itself (first line of the form code). The ACTION specifies the exact place of the CGI-program, in this case http://www.ebob42.com/cgi-bin/delbooks.exe (but kids don't try this at home, because this example is a link to my intranet, not the internet).
Of course, the "official" DELBOOKS.HTM contains even more controls (comboboxes, actually) to contain all possible titles, authors, publishers and ISBN-numbers of Delphi books that are inside the TDMBKS review database of Delphi books (with currently over 40 books inside). It's also available on the www at http://www.drbob42.com/reviews/tdmbks.htm, and when displayed on the Netscape Navigator it looks like this:
|
Clicking on the "Get Result"-button sends the information to the Web Server, which in its turn will start the delbooks.exe application (from the Forms' action information) with the information that was filled in in this form. In this case that would be DELPHI="2", LEVEL="3", TITLE="", AUTHOR="Bob_Swart", PUBLISHER="" and ISBN="" (note that spaces are replaced by underscores, as I found out the hard way). The delbooks.exe Delphi 2 CGI application needs to process the passed information, perform the query and generate a dynamic HTML-page on its standard output. The Web Server will then pass this dynamically generated HTML-page back to the Web Browser which will show it as the resulting page to the client.
Environment
Standard CGI applications need to look at the environment variables to know what kind of communication is used and how big the data in the standard input file is.
To obtain a list of environment variables with their value, I always use a simple component that I've written some time ago and now compiles with both Delphi 1 and 2 using a single {$IFDEF} statement.
unit TBDosEnv; interface uses SysUtils, WinTypes, WinProcs, Classes; type TBDosEnvironment = class(TComponent) public { Public class declarations (override) } constructor Create(AOwner: TComponent); override; destructor Destroy; override; private { Private field declarations } FDosEnvList: TStringList; protected { Protected method declarations } function GetDosEnvCount: Word; public { Public interface declarations } function GetDosEnvStr(Const Name: String): String; { This function is a modified version of the GetEnvVar function that appears in the WinDos unit that comes with Delphi. This function's interface uses Pascal strings instead of null-terminated strings. } property DosEnvCount: Word read GetDosEnvCount; property DosEnvList: TStringList read FDosEnvList; end; implementation constructor TBDosEnvironment.Create(AOwner: TComponent); var P: PChar; i: Integer; begin inherited Create(AOwner); FDosEnvList := TStringList.Create; {$IFDEF WIN32} P := GetEnvironmentStrings; {$ELSE} P := GetDosEnvironment; { Win API } {$ENDIF} i := 0; while P^ <> #0 do begin Inc(i); FDosEnvList.Add(StrPas(P)); Inc(P, StrLen(P)+1) { Fast Jump to Next Var } end; end {Create}; destructor TBDosEnvironment.Destroy; begin FDosEnvList.Free; FDosEnvList := nil; inherited Destroy end {Destroy}; function TBDosEnvironment.GetDosEnvCount: Word; begin Result := 0; if Assigned(FDosEnvList) then Result := FDosEnvList.Count end {GetDosEnvCount}; function TBDosEnvironment.GetDosEnvStr(Const Name: String): String; var i: Integer; Tmp: String; begin i := 0; Result := ''; if Assigned(FDosEnvList) then while i < fdosenvlist.count do begin Tmp := FDosEnvList[i]; Inc(i); if Pos(Name,Tmp) = 1 then begin Delete(Tmp,1,Length(Name)); if Tmp[1] = '=' then begin Delete(Tmp,1,1); Result := Tmp; i := FDosEnvList.Count { end while-loop } end end end end {GetDosEnvStr}; end.This component gets the list of environment variables when it is created. The properties DosEnvCount and DosEnvList are read-only, so best use is made when this component is created on the fly (and not dropped on a form), because only then a 'fresh' list of environment variables is obtained (and not loaded from the .DFM file for example).
Parsing
The environment variables the CGI application receives contain a section called REQUEST_METHOD.
This should be POST in our example (I won't go into other options).
Then we need to find the length of the information that was passed to us.
For that, we need to look for the CONTENT_LENGTH environment variable.
The actual information itself is passed as standard input file of length CONTENT_LENGTH (without an end-of-file marker, so we need to be sure not to read more than the specified number of characters).
The data on the standard input file consists of FIELD=VALUE pairs, separated by '&' tokens.
An example is: AUTHOR="Bob_Swart"&.
Once we have the complete input file in a long string buffer called Data, we can quickly scan for the value of AUTHOR by using the following function:
var Data: String; function Value(Const Field: ShortString): ShortString; var i: Integer; begin Result := ''; i := Pos(Field+'=',Data); if i > 0 then begin Inc(i,Length(Field)+1); while Data[i] <> '&' do begin Result := Result + Data[i]; Inc(i) end end end {Value};A similar function can be written if the data is numerical of nature (for example for the LEVEL field). The following skeleton code fragment shows how to dynamically create the TBDosEnvironment variable, read the information from the input file and get ready to parse the values for the controls on the form.
{$APPTYPE CONSOLE} var Data: String; ContentLength,i,j: Integer; begin writeln('HTTP/1.0 200 OK'); writeln('SERVER: Dr.Bob''s Intranet WebServer 1.0'); writeln('CONTENT-TYPE: TEXT/HTML'); writeln; writeln('<HTML>'); writeln('<BODY>'); writeln('<I>Generated by Dr.Bob''s CGI-Expert on </I>',DateTimeToStr(Now)); with TBDosEnvironment.Create(nil) do begin for i := 0 to Pred(DosEnvCount) do begin if Pos('REQUEST_METHOD',DosEnvList[i]) > 0 then begin Data := DosEnvList[i]; Delete(Data,1,Pos('=',Data)) end end; if Data = 'POST' then begin ContentLength := StrToInt(GetDosEnvStr('CONTENT_LENGTH')); SetLength(Data,ContentLength+1); j := 0; for i:=1 to ContentLength do begin Inc(j); read(Data[j]); end; Data[j+1] := '&'; { now call Value or ValueAsInteger to obtain individual values } end;Note the special three "writeln" lines that need to be written to the standard output to tell the Web Browser that the dynamically generated page it received is actually a correct HTTP page with TEXT/HTML contents.
Database
When writing data-driven CGI applications, you need some way to access your data.
One solution is simply to use the BDE and put your data in Paradox or dBASE tables.
However, if, for some reason, the BDE is not installed on your NT Web Server (maybe because your friendly neighbourhood Internet Provider doesn't provide you with the BDE in the first place), you need to improvise.
And since the BDE wasn't installed on our company's intranet Web Server either (until we installed IntraBuilder, that is), I had to improvise pretty early.
In the good old days of Borland Pascal, when we needed data in some kind of database we used a file of record.
And that's what we can use when we don't have access to the BDE: a plain old file-of-record.
All we need to do is write a program that will analyse a table structure, define some TRecord type and convert the data from the database to a file of TRecord.
Conversion
If we take a look at the general field types of Paradox, it shouldn't be hard to notice at least a few who would pose a problem when converting them to Pascal.
A Memo, for example, usually doesn't fit in a (Short) String.
And what about a Blob?
For those types who can be converted, I've defined the following translation table (just a rough translation that at least makes sure no information is lost during the one-way conversion):
Paradox field type | ObjectPascal conversion type |
TStringField (size) | String[length] |
TIntegerField, TWordField, TSmallIntField | Integer |
Currency | Double |
Memo, Blob | n/a (ignored) |
Using this translation table, it's not hard to write a simple program that takes a table as input and generate an ObjectPascal record type as output: the conversion record type:
{$APPTYPE CONSOLE} uses DB, DBTables; var i: Integer; begin if ParamCount >= 1 then with TTable.Create(nil) do try TableName := ParamStr(1); Active := True; writeln('Type'); writeln(' TRecord = record'); for i:=0 to Pred(FieldDefs.Count) do begin if (FieldDefs[i].FieldClass = TStringField) then writeln(' ':4,FieldDefs[i].Name,': String[',FieldDefs[i].Size,'];') else begin if (FieldDefs[i].FieldClass = TIntegerField) or (FieldDefs[i].FieldClass = TWordField) or (FieldDefs[i].FieldClass = TSmallintField) then writeln(' ':4,FieldDefs[i].Name,': Integer;') else if (FieldDefs[i].FieldClass = TCurrencyField) then writeln(' ':4,FieldDefs[i].Name,': Double;') else writeln('{ ':6,FieldDefs[i].Name,' }') end end finally writeln(' end;'); Free end else writeln('Usage: record tablename') end.Of course, the translation table and record program could be extended to include other Paradox field types as well, but for our example it is sufficient enough.
Records
One of the goals is to port the TDMBKS application to the internet.
And given the fact that we can write a Delphi 2 CGI application but without using the BDE, we're now faced with generating a record type for the delbooks.db table, and converting the actual records to a file-of-records.
Using the RECORD.EXE program from last section, the following record format was generated for the delbooks.db database:
Type TRecord = record ISBN: String[16]; Title: String[64]; Author: String[64]; Publisher: String[32]; Price: Double; Code: String[7]; { Comments } Level: Integer; TechnicalContentsQuality: Integer; QualityOfWriting: Integer; ValueForMoney: Integer; OverallAssessment: Integer; { Cover } end;Now, all we need to do is write a little while-not-eof-loop to walk through the database and extract the data from each record in the table and put it in a TRecord and write it to a file-of-TRecord. Given the fact that the fieldnames in the table are the same as the fieldnames of the TRecord, and we can use FieldByName to get to the field's values in the table, such as program consists of only a few lines of code (using the TRecord definition of the above listing):
{$APPTYPE CONSOLE} uses DB, DBTables, SysUtils; var i: Integer; Rec: TRecord; F: File of TRecord; begin if ParamCount >= 1 then with TTable.Create(nil) do try System.Assign(f,ChangeFileExt(ParamStr(1),'.REC')); Rewrite(f); TableName := ParamStr(1); Active := True; First; while not Eof do with Rec do begin ISBN := FieldByName('ISBN').AsString; Title := FieldByName('Title').AsString; Author := FieldByName('Author').AsString; Publisher := FieldByName('Publisher').AsString; Price := FieldByName('Price').AsFloat; Code := FieldByName('Code').AsString; Level := FieldByName('Level').AsInteger; TechnicalContentsQuality := FieldByName('TechnicalContentsQuality').AsInteger; QualityOfWriting := FieldByName('QualityOfWriting').AsInteger; ValueForMoney := FieldByName('ValueForMoney').AsInteger; OverallAssessment := FieldByName('OverallAssessment').AsInteger; write(f,Rec); Next end finally System.Close(f); Free end else writeln('Usage: convert tablename') end.This program can be used to convert the complete delbooks.db tables to delbooks.rec files-of-record with record of type TRecord inside. A Delphi 2 CGI program can simply open the file of record again and read the individual records without having to use the BDE. Of course, updating records is not easy, but for that we use the original database (and run the conversion program once we've done an update). After all, I only add new reviews once in a while to the database; like bi-monthly. So it won't matter much to depend on file-of-records for these queries.
Performance
One more difference between a CGI application that is able to use the BDE to obtain data and perform queries and our BDE-less CGI application is performance.
Not only is our application only about 70 KB in size, it doesn't need the BDE to be loaded, so the load time is much less (resulting in a much faster performance, especially since the complete time from start-up to shot-down counts).
In truth, practical CGI applications using the BDE often use an ISAPI (Information Server API) or NSAPI (Netscape Server API) extension to keep the CGI application "in the air" at all times.
In fact, I managed to increase performance even further by not using a file-of-record, but by using an array of records with pre-initialized values! Instead of writing to a file-of-record, I generated ObjectPascal code again with the values of the fields. That way, I could generate ObjectPascal source code for a CGI program with all information embedded. No file-of-record needed, and after compilation this was a stand-alone Delphi 2 executable (of 77824 bytes) that contained information of 44 books inside, capable of parsing environment variables, reading the standard input file, and generating a HTML-pages on the standard output with dynamic contents depending on the query information entered in the form. Surely, the only way to get faster than this is to go and fake the entire query and go back to static pages. The result, however, is a list combined of all books that have scored in one or more categories (fields on the query form). If no book has a score higher than 0, all relevant books (of the required version(s) of Delphi) are shown instead.
Scoring
The code used to score hits is pretty simple: for field on the Form for which a value was selected, we go through each record in the list of record and add one to the score for that particular record if the corresponding field in the record has a value that is equal to (or a sub-string of) the field from the input form.
For example, for the Author field, the code is as follows:
if DataRec.Author <> '' then begin {$IFDEF DEBUG} writeln('Author: ',DataRec.Author,'<BR>'); {$ENDIF} for i:=1 to Books16 do if Pos(DataRec.Author,Book16[i].Author) > 0 then Inc(Result16[i]); for i:=1 to Books32 do if Pos(DataRec.Author,Book32[i].Author) > 0 then Inc(Result32[i]) end;Note that the {$IFDEF DEBUG} can be used to write the value of the input field to the standard output, so we can actually use this CGI application to debug our Forms, which can be quite helpful if you think you must have some hits but none show up (because spaces are replaced by underscores, for example). Debugging your CGI applications is otherwise not easy, since you need a Web Server and Browser to interact for you...
Query Results
Let's take a look at the final part of the CGI application: the part where we're generating HTML code.
I'm using another advanced HTML-feature here, namely tables, to format the output in a nice way.
For every record that has a score of more than one, I write the score (number of hits), title, author, publisher, ISBN, level, technical contents, quality of writing, value for money and overall score.
I also include a link from the Title to a place where I know to have put a more detailed review.
This is great feature of dynamic HTML-pages: you can still include links to static HTML-pages, of course, so the result of the query is often the starting point for another set of jumps into hyperspace!
writeln('<HR>'); writeln('<P>'); writeln('<H3>The following books have been found for you:</H3>'); writeln('<TABLE BORDER>'); writeln('<TR>'); writeln('<TH><B>Hits</B></TH>'); writeln('<TH><B>Title</B></TH>'); writeln('<TH><B>Author</B></TH>'); writeln('<TH><B>Publisher</B></TH>'); writeln('<TH><B>ISBN</B></TH>'); writeln('<TH><B>Level</B></TH>'); writeln('<TH>Con</TH>'); writeln('<TH>Wri</TH>'); writeln('<TH>Val</TH>'); writeln('<TH><B>Tot</B></TH>'); writeln('</TR>');Once the table header has been written, it's time to go through the individual records. I didn't want to sort them, so for each possible score (from 5 to 1), I just go through the entire list of books and print the one with the current score. That way I know the books will be sorted on the number of hits, and are still in the order in which they were entered in the original delbooks.db database (which was sorted on level and an overall quality score). so generally, the books on top of the resulting output list are the best answer to the question that was asked.
if DataRec.Delphi2 then begin for Hits := 5 downto 1 do begin for i:=1 to Books32 do if Result32[i] = Hits then begin writeln('<TR>'); writeln('<TD>',Roman[Hits],'</TD>'); writeln('<TD><A HREF="',root32,Book32[i].HREF,'">',Book32[i].Title,'</A></TD>'); writeln('<TD>',Book32[i].Author,'</TD>'); writeln('<TD>',Book32[i].Publisher,'</TD>'); writeln('<TD>',Book32[i].ISBN,'</TD>'); writeln('<TD>',Level[Book32[i].Level],'</TD>'); writeln('<TD>',Book32[i].TechnicalContentsQuality,'</TD>'); writeln('<TD>',Book32[i].QualityOfWriting,'</TD>'); writeln('<TD>',Book32[i].ValueForMoney,'</TD>'); writeln('<TD><B>',Book32[i].OverallAssessment,'</B></TD>'); writeln('</TR>') end end; if DataRec.Delphi1 then writeln('<TR></TR>') end; if DataRec.Delphi1 then begin for Hits := 5 downto 1 do begin for i:=1 to Books16 do if Result16[i] = Hits then begin writeln('<TR>'); writeln('<TD>',Roman[Hits],'</TD>'); writeln('<TD><A HREF="',root16,Book16[i].HREF,'">',Book16[i].Title,'</A></TD>'); writeln('<TD>',Book16[i].Author,'</TD>'); writeln('<TD>',Book16[i].Publisher,'</TD>'); writeln('<TD>',Book16[i].ISBN,'</TD>'); writeln('<TD>',Level[Book16[i].Level],'</TD>'); writeln('<TD>',Book16[i].TechnicalContentsQuality,'</TD>'); writeln('<TD>',Book16[i].QualityOfWriting,'</TD>'); writeln('<TD>',Book16[i].ValueForMoney,'</TD>'); writeln('<TD><B>',Book16[i].OverallAssessment,'</B></TD>'); writeln('</TR>') end end end; writeln('</TABLE>'); writeln('<HR>'); writeln('<A HREF="http://www.drbob42.com">Dr.Bob''s Delphi Clinic</A>'); writeln('</BODY>'); writeln('</HTML>'); writeln; Free end
Debugging CGI
The resulting HTML-page, generated for the query we saw earlier, can be seen as we execute the CGI application.
Unfortunately, this would require a (personal) Web Server.
That's why I wrote a little CGI Debugger using Delphi 2.01 and the NetManage HTML control:
Personally, I find IntraBob to be a great help in the development of CGI applications!
Conclusion
I hope to have shown that we can write interactive internet (and intranet) CGI applications using Delphi 2 in a fairly straightforward (abeit non-visual) manner using CGI, WinCGI and Delphi 3 ISAPI/NSAPI Web Modules.
Personally, I plan to do a lot more with Delphi and the internet and intranet, so stay tuned for more news and technical stuff.