ICS has a web application server component which allows you to build dynamic web page very easily. Delphi code for each web page is encapsulated in a TUrlHandler class and compiled into your application, making a standalone webserver application.
In the article, we will create a TUrlHandler class which will read a DWScript script from disc and execute it. The script is responsible for build a valid answer which ICS will send back to the client.
A simple “Hello World” script is made of a single line like this:
Response.Write('Server time is ' + DateTimeToStr(Now) + ' ');
To invoke it, assuming the script is located in “Hello.pas”, the user must enter this URL into his browser:
http://localhost:20105/DWScripts/Hello.pas
Remember it is a script. You can change the file on disc and the changes will be immediately reflected for the next request, without recompiling your application. This looks much like PHP but use Delphi syntax instead of PHP. To be honest, PHP may use embedded HTML code within the script which is not supported here.
A more complex script may make use of request parameters. For example, the script “Add.pas” could looks like:
var Value1 : String; var Value2 : String; Response.ContentType := 'text/html'; Response.Status := '200 OK'; Response.Write(''); Response.Write('ICS and DWScript demo '); Response.Write('Server time is ' + DateTimeToStr(Now) + ' '); if not Request.CheckParamByName('Value1', Value1) then Response.Write('Missing Value1 parameter ') else if not Request.CheckParamByName('Value2', Value2) then Response.Write('Missing Value2 parameter ') else Response.Write(Value1 + ' + ' + Value2 + ' = ' + IntToStr(StrToIntDef(Value1, 0) + StrToIntDef(Value2, 0))); Response.Write('');
The URL to use in the browser looks like:
http://localhost:20105/DWScripts/Add.pas?Value1=123&Value2=456
Getting ICS and DWScript
ICS: http://wiki.overbyte.be/wiki/index.php/ICS_DownloadDWScript: http://code.google.com/p/dwscript/
Implementation
Less than 200 Delphi code is required to implement this behavior in an ICS based web application server. The code I will show you below can be plugged in OverbyteIcsWebAppServer demo application you can find in ICS V8 distribution. Two lines must be added to the main file in order to add the feature: add the unit in the uses clause and add a line to map “/DWScript/*” to the TUrlHandler taking care of the script.
Using the above wild card mapping, the TUrlHandler will be invoked for any URL beginning with “/DWScript/”. It will then access the full path to get the script filename. In the above examples, I used a “.pas” file extension for ease, but this is not mandatory at all. You can use any extensions and even no extension if you don’t like to have the user know you are using a DWScript.
In the implementation, I managed to have the DWScript not directly accessible thru an URL. This protects your source code. The user can’t access it. He can just execute it (Note that ICS THttpServer component has an option which allows access to any file, so what I just said maybe wrong).
The script has two objects instances readily available: “Request” and “Response”. They maps to the corresponding Delphi object instances and classes:
THttpResponse = class(TPersistent) private FStatus : String; FContentType : String; public DocStream : TStream; published property Status : String read FStatus write FStatus; property ContentType : String read FContentType write FContentType; procedure Write(const S : String); end; THttpRequest = class(TPersistent) public Params : String; class function ReadTextFile(const FileName : String) : String; published function GetParamByName(const ParamName: String): String; function CheckParamByName(const ParamName : String; var ParamValue : String): Boolean; end;
When using DWScript ExposeRTTI function, you are exposing a Delphi class. With the options I selected, only the published methods and properties will be exposed to the script.
THttpResponse exposes Status and ContentType properties as well as Write method. The two properties allows the script to select the HTTP status return and the HTTP content type. They default to “200 OK” and “text/html” which are the most common values. You can use anything you need for your application.
The Write method will be used by the script to produce the document part of the HTTP response sent back to the client. Obviously, the document format must match the content type.
THttpRequest exposes the incoming request. Using it you may access the parameters passed thru the URL. One method gets a parameter value when you provides his name while the other return the value as well as a Boolean value telling if the parameter exists or not.
The most important part of the code is a TUrlHandler derived class. I named it TUrlHandlerDWScript. His declaration looks like this:
TUrlHandlerDWScript = class(TUrlHandler) protected FScript : IdwsProgram; FCompileMsgs : String; FDelphiWebScript : TDelphiWebScript; FUnit : TdwsUnit; FExec : IdwsProgramExecution; FHttpRequest : THttpRequest; FHttpResponse : THttpResponse; procedure ExposeInstancesAfterInitTable(Sender: TObject); public procedure Execute; override; end;
The five first member variables are required for DWScript operation. You should look at DWScript documentation for help about their use.
The last two member variables are the object instances we talk above. Their published parts are exposed to the script.
There is also an event handler ExposeInstancesAfterInitTable which is required by DWScript engine to expose the Delphi object instances to the script.
Finally, there is a single method which is called by ICS web application server framework to handle the mapped URL. This is where almost everything happens.
procedure TUrlHandlerDWScript.Execute; var SrcFileName : String; Source : String; begin FDelphiWebScript := TDelphiWebScript.Create(nil); FUnit := TdwsUnit.Create(nil); FHttpResponse := THttpResponse.Create; FHttpRequest := THttpRequest.Create; try DocStream.Free; DocStream := TMemoryStream.Create; FHttpResponse.DocStream := DocStream; FHttpResponse.Status := '200 OK'; FHttpResponse.ContentType := 'text/html'; FHttpRequest.Params := Params; FUnit.OnAfterInitUnitTable := ExposeInstancesAfterInitTable; FUnit.UnitName := 'WebPage'; FUnit.Script := FDelphiWebScript; FUnit.ExposeRTTI(TypeInfo(THttpResponse), [eoNoFreeOnCleanup]); FUnit.ExposeRTTI(TypeInfo(THttpRequest), [eoNoFreeOnCleanup]); // In this application, we have placed DWScripts source code in // a directory at the same level as the "Template" folder. SrcFileName := ExcludeTrailingPathDelimiter( ExtractFilePath(Client.TemplateDir)) + StringReplace(Client.Path, '/', '\', [rfReplaceAll]); if not FileExists(SrcFileName) then FHttpResponse.Write('Script not found') else begin Source := FHttpRequest.ReadTextFile(SrcFileName); FScript := FDelphiWebScript.Compile(Source); FCompileMsgs := FScript.Msgs.AsInfo; if FScript.Msgs.HasErrors then begin FHttpResponse.Write('' + FCompileMsgs + ''); end else begin FExec := FScript.Execute; end; end; AnswerStream(FHttpResponse.Status, FHttpResponse.ContentType, NO_CACHE); finally FreeAndNil(FUnit); FreeAndNil(FDelphiWebScript); FreeAndNil(FHttpResponse); FreeAndNil(FHttpRequest); end; Finish; end;
Basically the code creates all the required object instances, initializes default values, expose Delphi classes, read the script source code from file, compile the script and either create an error message should any compilation fails, or execute the script so that it can produce the document. Finally, the document is sent back and all object instances are destroyed.
Full source code:
The code is available from my website, see
http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
unit OverbyteIcsWebAppServerDWScriptUrlHandler; interface uses Classes, SysUtils, OverbyteIcsHttpAppServer, OverbyteIcsHttpSrv, dwsVCLGUIFunctions, dwsMagicExprs, dwsRTTIExposer, dwsFunctions, dwsSymbols, dwsExprs, dwsComp; type THttpResponse = class(TPersistent) private FStatus : String; FContentType : String; public DocStream : TStream; published property Status : String read FStatus write FStatus; property ContentType : String read FContentType write FContentType; procedure Write(const S : String); end; THttpRequest = class(TPersistent) public Params : String; class function ReadTextFile(const FileName : String) : String; published function GetParamByName(const ParamName: String): String; function CheckParamByName(const ParamName : String; var ParamValue : String): Boolean; end; TUrlHandlerDWScript = class(TUrlHandler) protected FScript : IdwsProgram; FCompileMsgs : String; FDelphiWebScript : TDelphiWebScript; FUnit : TdwsUnit; FExec : IdwsProgramExecution; FHttpRequest : THttpRequest; FHttpResponse : THttpResponse; procedure ExposeInstancesAfterInitTable(Sender: TObject); public procedure Execute; override; end; implementation {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} { TUrlHandlerDWScript } {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} procedure TUrlHandlerDWScript.Execute; var SrcFileName : String; Source : String; begin FDelphiWebScript := TDelphiWebScript.Create(nil); FUnit := TdwsUnit.Create(nil); FHttpResponse := THttpResponse.Create; FHttpRequest := THttpRequest.Create; try DocStream.Free; DocStream := TMemoryStream.Create; FHttpResponse.DocStream := DocStream; FHttpResponse.Status := '200 OK'; FHttpResponse.ContentType := 'text/html'; FHttpRequest.Params := Params; FUnit.OnAfterInitUnitTable := ExposeInstancesAfterInitTable; FUnit.UnitName := 'WebPage'; FUnit.Script := FDelphiWebScript; FUnit.ExposeRTTI(TypeInfo(THttpResponse), [eoNoFreeOnCleanup]); FUnit.ExposeRTTI(TypeInfo(THttpRequest), [eoNoFreeOnCleanup]); // In this application, we have placed DWScripts source code in // a directory at the same level as the "Template" folder. SrcFileName := ExcludeTrailingPathDelimiter( ExtractFilePath(Client.TemplateDir)) + StringReplace(Client.Path, '/', '\', [rfReplaceAll]); if not FileExists(SrcFileName) then FHttpResponse.Write('Script not found') else begin Source := FHttpRequest.ReadTextFile(SrcFileName); FScript := FDelphiWebScript.Compile(Source); FCompileMsgs := FScript.Msgs.AsInfo; if FScript.Msgs.HasErrors then begin FHttpResponse.Write('' + FCompileMsgs + ''); end else begin FExec := FScript.Execute; end; end; AnswerStream(FHttpResponse.Status, FHttpResponse.ContentType, NO_CACHE); finally FreeAndNil(FUnit); FreeAndNil(FDelphiWebScript); FreeAndNil(FHttpResponse); FreeAndNil(FHttpRequest); end; Finish; end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} procedure TUrlHandlerDWScript.ExposeInstancesAfterInitTable(Sender: TObject); begin FUnit.ExposeInstanceToUnit('Response', 'THttpResponse', FHttpResponse); FUnit.ExposeInstanceToUnit('Request', 'THttpRequest', FHttpRequest); end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} { THttpResponse } {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} procedure THttpResponse.Write(const S: String); var Ch : Char; B : Byte; begin for Ch in S do begin // We should convert the Unicode string to whatever the document // is supposed to be. Here we just convert it, brute force, to ASCII. // This won't work eastern character sets. B := Ord(AnsiChar(Ch)); DocStream.Write(B, 1); end; end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} { THttpRequest } {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} function THttpRequest.CheckParamByName( const ParamName : String; var ParamValue : String): Boolean; begin Result := ExtractURLEncodedValue(Params, ParamName, ParamValue); end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} function THttpRequest.GetParamByName(const ParamName: String): String; begin ExtractURLEncodedValue(Params, ParamName, Result); end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} class function THttpRequest.ReadTextFile(const FileName : String) : String; var Stream : TFileStream; AnsiBuf : AnsiString; begin Stream := TFileStream.Create(FileName, fmOpenRead); try SetLength(AnsiBuf, Stream.Size); Stream.Read(AnsiBuf[1], Stream.Size); Result := String(AnsiBuf); finally FreeAndNil(Stream); end; end; {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} end.
Download source code from: http://www.overbyte.be/frame_index.html?redirTo=/blog_source_code.html
Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be
1 comment:
To embed the script in HTML (just like PHP), you can use the TdwsHTMLFilter component.
Just link the html filter component to your TDelphiWebScript, and you can optionnally adjust the opening and closing tags.
For higher performance, it is also recommanded to compile each script only once, and then create only executions when needed (a single IdwsProgram can have multiple executions, in different threads).
Post a Comment