-
Notifications
You must be signed in to change notification settings - Fork 12
Lazarus Server
Usually ExtJs applications interact with back end servers that provide Json or XML data. Those servers can be created in virtually any programming language, such as PHP, Perl, Java, Ruby, C++, Pascal, and even Bash. I'll use Lazarus, the IDE and framework for the FreePascal compiler.
I'll assume you understand the basic concepts behind FreePascal and Lazarus, and that you have read and tested the examples provided in this wiki.
Remark. It is important that you install the package Weblaz in Lazarus. If you don't know how to install a package, please read this. In Lazarus, go to Package->Install/Deinstall packages..., find "weblaz" on the bottom of the right side list, select it and click "install selection", then click on Save and Rebuild IDE.
Weblaz is a package that allows us to create web applications, such as CGI, FastCGI, Apache modules and Stand Alone webservers. It allows the inclusion of Webmodules, that lets divide the application in parts. Each WebModule has a list of "Actions" that can be called from the web browser as GET and POST methods.
Example
Imagine a basic CGI application called "test.cgi" running on an Apache web server. To reach this application, the user should type this on the web browser:
http://localhost/cgi-bin/test.cgi
Now, imagine the application can handle requests related to two kinds of data, for example customer data and user data. Instead of writing one class or unit containing methods to handle both types of data, theres a clean approach, the use of one WebModule for Customer data and other WebModule for User data.
If there are two web modules, one named customers and other named users, to reach them from the browser, the user should type:
http://localhost/cgi-bin/test.cgi/customers
http://localhost/cgi-bin/test.cgi/users
Now, imagine you want to add CRUD operations to customers. In the customer webmodule just start adding actions, such as "insertCustomer", "updateCustomer", "deleteCustomer", "getCustomer" and "getCustomerList". The same for users.
To reach those methods, in the web browser you should type this:
http://localhost/cgi-bin/test.cgi/customers/getCustomerList
http://localhost/cgi-bin/test.cgi/customers/getCustomer
http://localhost/cgi-bin/test.cgi/customers/insertCustomer
http://localhost/cgi-bin/test.cgi/customers/deleteCustomer
http://localhost/cgi-bin/test.cgi/customers/updateCustomer
In this stage, I'll explain how to create a basic cgi application, with one Webmodule and two basic actions that return Json data to be handled by our ExtJs application.
To start, please open Lazarus and go to Project->New project, then select "CGI Application" (this will be shown only if you installed the Weblaz package", then click Ok. This will create the basic structure of our project, please go to Project->Save Project as... and choose a directory to store the source files, then set the name for the project as "ar_better_crm.lpi". After saving the project's name, we are asked for defining a name for the file "unit1.pas", this file will be the handler for Customer related requests, so, please name it "customer_handler.pas".
Here is a screenshot of the files after saved:
To compile the project, just press CTRL+F9 or Execute->Build. The result will be a couple of .o, .ppu, .compiled files, and an executable called "a_better_crmlpi" on Linux/Unix or "a_better_crmlpi.exe" on Windows. I don't like the name that Lazaus created for the executable, so, please go to Project->Project Options...->Compiler Options->Name of final file (-o) and write "a_better_crm", then click Ok.
Again, CTRL+F9 and look at the generated file list, should be a "a_better_crm" file created.
Ok, now it's time to deploy the executable to our web server, you can copy the executable to /var/www/cgi-bin or create a symlink, to avoid copying every time the app is compiled.
To create a symlink, please do this (on linux):
sudo ln -s /current/path/a_better_crm /var/www/cgi-bin
Instead of /current/path you have to type in the complete path to your executable file.
Now it's time to test the results, please open your web browser and type http://localhost/cgi-bin/a_better_crm, the result should be this:
Now, go back to Lazarus and take a look at the file "customer_handler.pas". It should look as shown in this screenshot.
As the screenshot shows, there's a class named "TFPWebModule1", this is the default name Lazarus creates for our WebModules, to rename it, please open the "Object Inspector" (View->Object Inspector) and select FPWebModule1: TFPWebModule1 and edit its Name property, I'll name it "CustomerHandler". The code should change to this:
Code view
unit customer_handler;
{$mode objfpc}{$H+}
interface
uses
SysUtils, Classes, httpdefs, fpHTTP, fpWeb;
type
TCustomerHandler = class(TFPWebModule)
private
{ private declarations }
public
{ public declarations }
end;
var
CustomerHandler: TCustomerHandler;
implementation
{$R *.lfm}
initialization
RegisterHTTPModule('TFPWebModule1', TCustomerHandler);
end.
Now its time to take a look at the "initialization" section of this code. The RegisterHTTPModule procedure defines how the module has to be referenced, here the default name is "TFPWebModule1", so the user has to point the browser to:
http://localhost/cgi-bin/a_better_crm/TFPWebModule1
As this module should contain all customer related actions, I'll replace "TFPWebModule1" to "customers" (all lowercase), please do that.
Now, again go to the Object Inspector, click CustomerHandler and look at the properties, this time click on the Events tab, then double click on the OnRequest handler, the method DataModuleRequest will be automatically created, edit the method to look like this:
procedure TCustomerHandler.DataModuleRequest(Sender: TObject;
ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
AResponse.Content := 'Hello World!';
Handled := True;
end;
Now recompile, then point your browser to:
http://localhost/cgi-bin/a_better_crm/customers
The "Hello World!" message should appear, if not, please review all the steps.
As I mentioned before, actions are handlers for GET and POST http requests. To create an action, please go to the Object Inspector->CustomerHandler->Actions and click the "..." button, a Dialog with the options to Add or Remove actions will appear, click Add. An action named "TFPWebAction0" will be created, please rename it to "getCustomerList" (case sensitive), and click on Events tab and double click on OnRequest, this will create the method "getCustomerListRequest", please edit like this:
procedure TCustomerHandler.getCustomerListRequest(Sender: TObject;
ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
AResponse.Content := 'Hello, I am getCustomerList action!';
Handled := Truwe;
end;
Before testing in the web browser, please change the DataModuleRequest action to this:
procedure TCustomerHandler.DataModuleRequest(Sender: TObject;
ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
AResponse.Content := 'Hello World!';
Handled := False;
end;
Or just remove it.
Now, compile, then point your browser to:
http://localhost/cgi-bin/a_better_crm/customers/getCustomerList
You should see the "Hello, I am getCustomerList action!" message, if not, please double check every step.
In the Sencha Designer section I explained to you to create a static json file in the http server's documentRoot, now please move that file to /var/www/data.json to /var/www/cgi-bin/data.json, to let the CGI program acces it.
After the file is moved, please add the units fpJson and jsonparser to the "uses" clause of customer_handler.pas, and re-edit the getCustomerListRequest method to look like this:
Code view
procedure TCustomerData.getCustomerListRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
var
lJSonParser: TJSONParser;
lStr: TFileStream;
lFile: string;
lJSON: TJSONObject;
begin
lFile := 'data.json';
lStr := TFileStream.Create(lFile, fmOpenRead);
lJSonParser := TJSONParser.Create(lStr);
try
lJSON := lJSonParser.Parse as TJSonObject;
AResponse.Content := lJSON.AsJSON;
finally
lJSonParser.Free;
lStr.Free;
end;
Handled := True;
end;
Again, compile and test in your web browser, the result should be this:
Note. If your json data includes utf8 characters, please add this before AResponse.Content:
AResponse.ContentType := 'text/html;charset=utf-8';
To start using the CGI provided data, instead of the static file, please open your project on Sencha Designer, then go to Project Inspector->Stores->Customers->MyAjaxProxy and change the "url" property to:
/cgi-bin/a_better_crm/customers/getCustomerList
Save, Deploy and test.
The url parameter we've set in the previous paragraph, allows to read data from the server, but what about Insert/Update and Delete?.
The ajax proxy provides a nice method to handle CRUD operations, to use it, just paste this in the "api" property of MyAjaxProxy, and later clean the url property:
MyAjaxProxy->api:
{
read: "/cgi-bin/a_better_crm/customers/read",
create: "/cgi-bin/a_better_crm/customers/insert",
update: "/cgi-bin/a_better_crm/customers/update",
destroy: "/cgi-bin/a_better_crm/customers/delete"
}
Important!. On Windows you must replace /cgi-bin/a_better_crm/customers/... with /cgi-bin/a_better_crm.exe/customers/...
As you can see, when the Store.load() method is called, the url /cgi-bin/a_better_crm/customers/read is executed. As we don't have a "read" method on our "customers" WebModule, we'll rename "getCustomerList" action to just "read".
Now, to create an Update handler, just add a new action in your WebModule. Let's call it "update" with an OnRequest event handler also named "update", then add this code:
Code view
procedure TCustomerHandler.update(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
var
lResponse: TJSONObject;
lJSON: TJSONObject;
lResult: TJSONObject;
lArray: TJSONArray;
lJSonParser: TJSONParser;
lStr: TStringList;
lFile: string;
I: Integer;
begin
(* Load "database" *)
lFile := 'data.json';
lStr := TStringList.Create;
lStr.LoadFromFile(lFile);
lJSonParser := TJSONParser.Create(lStr.Text);
lResponse :=TJSONObject.Create;
try
try
(* Assign values to local variables *)
lResult := TJSONParser.Create(ARequest.Content).Parse as TJSONObject;
(* Traverse data finding by Id *)
lJSON := lJSonParser.Parse as TJSonObject;
lArray := lJSON.Arrays['root'];
for I := 0 to lArray.Count - 1 do
begin
if lResult.Integers['id'] = (lArray.Objects[I] as TJsonObject).Integers['id'] then
begin
(* Assign field values *)
(lArray.Objects[I] as TJsonObject).Strings['name'] := lResult.Strings['name'];
(lArray.Objects[I] as TJsonObject).Strings['email'] := lResult.Strings['email'];
(lArray.Objects[I] as TJsonObject).Integers['age'] := lResult.Integers['age'];
(lArray.Objects[I] as TJsonObject).Integers['gender'] := lResult.Integers['gender'];
(lArray.Objects[I] as TJsonObject).Booleans['active'] := lResult.Booleans['active'];
lStr.Text := lJSon.AsJSON;
lStr.SaveToFile(lFile);
Break;
end;
end;
lResponse.Add('success', true);
lResponse.Add('root', lResult);
AResponse.ContentType := 'text/json;charset=utf-8';
AResponse.Content := lResponse.AsJSON;
except
on E: Exception do
begin
lResponse.Add('success', false);
lResponse.Add('msg', E.Message);
AResponse.Content := lResponse.AsJSON;
end;
end;
finally
lResponse.Free;
lJSonParser.Free;
lStr.Free;
end;
Handled := True;
end;
This looks like a bunch of code, and it is, because we are handling all the file access directly. You can improve this code by replacing the data.json file by something more practical, such an SQL database.
Now go back to Sencha Designer and edit the action onOkClick of CustomerProperties controller with this code:
Code view
onOkClick: function(button, e, options) {
var win = button.up('window');
frm = win.down('form').getForm();
var store = this.getCustomersStore();
if(mode=="Insert") {
customer = frm.getValues();
store.insert(0, customer);
}
else
{
customer = frm.getRecord();
frm.updateRecord(customer);
}
store.sync();
win.destroy();
}
In Lazarus, let's add a new action to the customers WebModule, the new action will be called "insert" and the OnRequest event will be also called "insert".
Code view
procedure TCustomerHandler.insert(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
var
lResponse: TJSONObject;
lJSON: TJSONObject;
lResult: TJSONObject;
lArray: TJSONArray;
lJSonParser: TJSONParser;
lStr: TStringList;
lFile: string;
begin
Randomize;
(* Load "database" *)
lFile := 'data.json';
lStr := TStringList.Create;
lStr.LoadFromFile(lFile);
lJSonParser := TJSONParser.Create(lStr.Text);
lResponse :=TJSONObject.Create;
try
try
lJSON := lJSonParser.Parse as TJSonObject;
lArray := lJSON.Arrays['root'];
(* Assign values to local variables *)
lResult := TJSONParser.Create(ARequest.Content).Parse as TJSONObject;
lResult.Integers['id'] := Random(1000);
lArray.Add(lResult);
(* Save the file *)
lStr.Text:= lJSON.AsJSON;
lStr.SaveToFile(lFile);
lResponse.Add('success', true);
lResponse.Add('root', lResult);
AResponse.Content := lResponse.AsJSON;
except
on E: Exception do
begin
lResponse.Add('success', false);
lResponse.Add('msg', E.Message);
AResponse.Content := lResponse.AsJSON;
end;
end;
finally
lResponse.Free;
lJSonParser.Free;
lStr.Free;
end;
Handled := True;
end;
To implement the Delete action, do the same process of adding a new action called "delete" with an OnRequest handler named "delete" with this code:
Code view
procedure TCustomerHandler.delete(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
var
lResponse: TJSONObject;
lJSON: TJSONObject;
lResult: TJSONObject;
lArray: TJSONArray;
lJSonParser: TJSONParser;
lStr: TStringList;
lFile: string;
I: Integer;
begin
(* Load "database" *)
lFile := 'data.json';
lStr := TStringList.Create;
lStr.LoadFromFile(lFile);
lJSonParser := TJSONParser.Create(lStr.Text);
lResponse :=TJSONObject.Create;
try
try
(* Assign values to local variables *)
lResult := TJSONParser.Create(ARequest.Content).Parse as TJSONObject;
(* Traverse data finding by Id *)
lJSON := lJSonParser.Parse as TJSonObject;
lArray := lJSON.Arrays['root'];
for I := 0 to lArray.Count - 1 do
begin
if lResult.Integers['id'] = (lArray.Objects[I] as TJsonObject).Integers['id'] then
begin
lArray.Delete(I);
lStr.Text := lJSon.AsJSON;
lStr.SaveToFile(lFile);
Break;
end;
end;
lResponse.Add('success', true);
lResponse.Add('root', lResult);
AResponse.Content := lResponse.AsJSON;
except
on E: Exception do
begin
lResponse.Add('success', false);
lResponse.Add('msg', E.Message);
AResponse.Content := lResponse.AsJSON;
end;
end;
finally
lResponse.Free;
lJSonParser.Free;
lStr.Free;
end;
Handled := True;
end;
Also, in Sencha Designer, please review the CustomerGrid controller and its onDeleteClick method:
Code view
onDeleteClick: function(button, e, options) {
Ext.Msg.show({
title:'Delete record?',
msg: 'Please configrm',
buttons: Ext.Msg.YESNO,
icon: Ext.Msg.QUESTION,
fn: function(btn, text) {
if(btn == 'yes') {
record = button.up('gridpanel').getSelectionModel().getSelection()[0];
var store = this.getCustomersStore();
store.remove(record);
store.sync();
}
},
scope: this
});
}
Until now, I've explained how to create a module named "customers" that handles customer CRUD opperations. Now I'll explain how to handle the Login operation.
Please, in Lazarus go to File->New...->Module->Web Module. This will create a new unit called "unit1" or something similar, please go to File->Save... and save this unit as "login_handler".
After the new unit is created, go to the Object Inspector and rename the new module called "TFPWebModule1" to "LoginHandler". Then go to the bottom of the source file and replace the RegisterHTTPModule directive to:
RegisterHTTPModule('login', TLoginHandler);
Then, go back to the Object Inspector and click on LoginHandler, then click on Actions, this will open the Actions dialog, please add a new action and set its Name property to "check", then go to Events and double click at the right of "OnRequest" to create the checkRequest handler.
The content of the new event is this:
Code View
procedure TLoginHandler.checkRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
var
lJSON: TJSONObject;
begin
try
lJSON := TJSONObject.Create;
if (ARequest.ContentFields.Values['userName']='admin') and (ARequest.ContentFields.Values['passWord']='admin') then
begin
lJSON.Add('success', True);
end
else
begin
lJSON.Add('failure', True);
lJSON.Add('msg', 'Incorrect User or Password.');
end;
AResponse.ContentType := 'application/json; charset=utf-8';
AResponse.Content := AnsiToUtf8(lJSON.AsJSON);
finally
lJSON.Free;
end;
Handled := True;
end;
This event receives form data sent by the ExtJs application, and checks for the values of "userName" and "passWord" fields. If they are "admin" and "admin", it returns a "success" value, if not, it returns a "failure" with a message.
That's it.
The new unit should look like this:
Code View
unit login_handler;
{$mode objfpc}{$H+}
interface
uses
SysUtils, Classes, httpdefs, fpHTTP, fpWeb, fpjson;
type
{ TLoginHandler }
TLoginHandler = class(TFPWebModule)
procedure checkRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
private
{ private declarations }
public
{ public declarations }
end;
var
LoginHandler: TLoginHandler;
implementation
{$R *.lfm}
{ TLoginHandler }
procedure TLoginHandler.checkRequest(Sender: TObject; ARequest: TRequest;
AResponse: TResponse; var Handled: Boolean);
var
lJSON: TJSONObject;
begin
try
lJSON := TJSONObject.Create;
if (ARequest.ContentFields.Values['userName']='admin') and (ARequest.ContentFields.Values['passWord']='admin') then
begin
lJSON.Add('success', True);
end
else
begin
lJSON.Add('failure', True);
lJSON.Add('msg', 'Incorrect User or Password.');
end;
AResponse.ContentType := 'application/json; charset=utf-8';
AResponse.Content := AnsiToUtf8(lJSON.AsJSON);
finally
lJSON.Free;
end;
Handled := True;
end;
initialization
RegisterHTTPModule('login', TLoginHandler);
end.
Now, please go to Sencha Designer and click on the Login controller, then find its "onLoginClick" event, and replace the old content with this:
Code View
var win = button.up('loginform');
var frm = win.getForm();
frm.submit({
success: function(form, action){
console.log('success');
var UserController = this.getController('MyApp.controller.User');
this.getController('MyApp.controller.Main').showMainView();
UserController.saveSession();
win.destroy();
},
failure: function(form, action){
switch(action.failureType){
case Ext.form.Action.CLIENT_INVALID:
Ext.Msg.alert('Failure', 'Please complete the required fields.');
break;
case Ext.form.Action.CONNECT_FAILURE:
Ext.Msg.alert('Failure', 'Ajax communication failed.');
break;
case Ext.form.Action.SERVER_INVALID:
Ext.Msg.alert('Failure', action.result.msg);
break;
}
},
scope: this
});
After this, you'll have to point the "url" property of the LoginForm View to: /cgi-bin/a_better_crm/login/check.
This was the last part of the tutorial. I hope you enjoyed and learned as much as I did writing it.
Leonardo.