mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-15 07:45:54 +01:00
Improved JSON RPC 2.0 Client Sample
This commit is contained in:
parent
df0479d251
commit
ac654658f1
27
.gitignore
vendored
27
.gitignore
vendored
@ -49,3 +49,30 @@ Expert/
|
||||
*.~34~
|
||||
*.~35~
|
||||
*.~36~
|
||||
*.~63~
|
||||
*.~64~
|
||||
*.~65~
|
||||
*.~66~
|
||||
*.~67~
|
||||
*.~68~
|
||||
*.~69~
|
||||
*.~41~
|
||||
*.~42~
|
||||
*.~43~
|
||||
*.~44~
|
||||
*.~45~
|
||||
*.~46~
|
||||
*.~47~
|
||||
*.~48~
|
||||
*.~49~
|
||||
*.~50~
|
||||
*.~51~
|
||||
/samples/serversentevents2/entities/__history/*.~70~
|
||||
/samples/serversentevents2/entities/__history/*.~71~
|
||||
/samples/serversentevents2/entities/__history/*.~72~
|
||||
/samples/serversentevents2/entities/__history/*.~73~
|
||||
/samples/serversentevents2/entities/__history/*.~74~
|
||||
/samples/serversentevents2/*.res
|
||||
*.~39~
|
||||
*.~40~
|
||||
*.~20~
|
||||
|
@ -201,6 +201,7 @@ resourcestring
|
||||
|
||||
sCRUDMethodsIntf =
|
||||
sLineBreak +
|
||||
' public' + sLineBreak +
|
||||
' //Sample CRUD Actions for a "Customer" entity' + sLineBreak +
|
||||
' [MVCPath(''/customers'')]' + sLineBreak +
|
||||
' [MVCHTTPMethod([httpGET])]' + sLineBreak +
|
||||
|
@ -65,7 +65,6 @@ end;
|
||||
|
||||
function TArticlesService.GetByID(const AID: Integer): TArticle;
|
||||
begin
|
||||
Result := nil;
|
||||
FDM.dsArticles.Open('SELECT * FROM ARTICOLI WHERE ID = :ID', [AID]);
|
||||
try
|
||||
if not FDM.dsArticles.Eof then
|
||||
|
@ -26,7 +26,6 @@ uses Controllers.Articles, MVCFramework.Middleware.CORS;
|
||||
|
||||
{$R *.dfm}
|
||||
|
||||
|
||||
procedure TWebModule1.WebModule1DefaultHandlerAction(Sender: TObject;
|
||||
Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
|
||||
begin
|
||||
|
@ -8,18 +8,13 @@ uses
|
||||
IdHTTPWebBrokerBridge,
|
||||
Web.WebReq,
|
||||
Web.WebBroker,
|
||||
WebModuleUnit1 in 'WebModuleUnit1.pas' {WebModule1: TWebModule} ,
|
||||
WebModuleUnit1 in 'WebModuleUnit1.pas' {WebModule1: TWebModule},
|
||||
Controllers.Base in 'Controllers.Base.pas',
|
||||
Controllers.Articles in 'Controllers.Articles.pas',
|
||||
Services in 'Services.pas',
|
||||
BusinessObjects in 'BusinessObjects.pas',
|
||||
MainDM in 'MainDM.pas' {dmMain: TDataModule} ,
|
||||
Commons in 'Commons.pas',
|
||||
MVCFramework.Serializer.JSON in '..\..\sources\MVCFramework.Serializer.JSON.pas',
|
||||
MVCFramework.Commons in '..\..\sources\MVCFramework.Commons.pas',
|
||||
MVCFramework.Serializer.Intf in '..\..\sources\MVCFramework.Serializer.Intf.pas',
|
||||
MVCFramework.FireDAC.Utils in '..\..\sources\MVCFramework.FireDAC.Utils.pas',
|
||||
MVCFramework.DataSet.Utils in '..\..\sources\MVCFramework.DataSet.Utils.pas';
|
||||
MainDM in 'MainDM.pas' {dmMain: TDataModule},
|
||||
Commons in 'Commons.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
|
@ -104,11 +104,6 @@
|
||||
<DesignClass>TDataModule</DesignClass>
|
||||
</DCCReference>
|
||||
<DCCReference Include="Commons.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Serializer.JSON.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Commons.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Serializer.Intf.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.FireDAC.Utils.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.DataSet.Utils.pas"/>
|
||||
<None Include="ModelSupport_ordersmanager\default.txvpck"/>
|
||||
<None Include="ModelSupport_ordersmanager\Services\default.txvpck"/>
|
||||
<None Include="ModelSupport_ordersmanager\ordersmanager\default.txvpck"/>
|
||||
|
Binary file not shown.
@ -2,8 +2,8 @@ object Form10: TForm10
|
||||
Left = 0
|
||||
Top = 0
|
||||
Caption = 'Form10'
|
||||
ClientHeight = 482
|
||||
ClientWidth = 535
|
||||
ClientHeight = 544
|
||||
ClientWidth = 508
|
||||
Color = clBtnFace
|
||||
Font.Charset = DEFAULT_CHARSET
|
||||
Font.Color = clWindowText
|
||||
@ -14,24 +14,24 @@ object Form10: TForm10
|
||||
PixelsPerInch = 96
|
||||
TextHeight = 13
|
||||
object edtValue1: TEdit
|
||||
Left = 16
|
||||
Top = 24
|
||||
Left = 8
|
||||
Top = 8
|
||||
Width = 89
|
||||
Height = 21
|
||||
TabOrder = 0
|
||||
Text = '42'
|
||||
end
|
||||
object edtValue2: TEdit
|
||||
Left = 111
|
||||
Top = 24
|
||||
Left = 103
|
||||
Top = 8
|
||||
Width = 89
|
||||
Height = 21
|
||||
TabOrder = 1
|
||||
Text = '10'
|
||||
end
|
||||
object btnSubstract: TButton
|
||||
Left = 206
|
||||
Top = 22
|
||||
Left = 198
|
||||
Top = 6
|
||||
Width = 91
|
||||
Height = 25
|
||||
Caption = 'Subtract'
|
||||
@ -39,24 +39,24 @@ object Form10: TForm10
|
||||
OnClick = btnSubstractClick
|
||||
end
|
||||
object edtResult: TEdit
|
||||
Left = 303
|
||||
Top = 24
|
||||
Left = 295
|
||||
Top = 8
|
||||
Width = 74
|
||||
Height = 21
|
||||
ReadOnly = True
|
||||
TabOrder = 3
|
||||
end
|
||||
object edtReverseString: TEdit
|
||||
Left = 16
|
||||
Top = 72
|
||||
Left = 8
|
||||
Top = 56
|
||||
Width = 184
|
||||
Height = 21
|
||||
TabOrder = 4
|
||||
Text = 'Daniele Teti'
|
||||
end
|
||||
object btnReverseString: TButton
|
||||
Left = 206
|
||||
Top = 70
|
||||
Left = 198
|
||||
Top = 54
|
||||
Width = 91
|
||||
Height = 25
|
||||
Caption = 'Reverse String'
|
||||
@ -64,23 +64,23 @@ object Form10: TForm10
|
||||
OnClick = btnReverseStringClick
|
||||
end
|
||||
object edtReversedString: TEdit
|
||||
Left = 303
|
||||
Top = 72
|
||||
Width = 210
|
||||
Left = 295
|
||||
Top = 56
|
||||
Width = 202
|
||||
Height = 21
|
||||
ReadOnly = True
|
||||
TabOrder = 6
|
||||
end
|
||||
object edtFilter: TEdit
|
||||
Left = 16
|
||||
Top = 128
|
||||
Left = 8
|
||||
Top = 245
|
||||
Width = 184
|
||||
Height = 21
|
||||
TabOrder = 7
|
||||
end
|
||||
object edtGetCustomers: TButton
|
||||
Left = 206
|
||||
Top = 126
|
||||
Left = 198
|
||||
Top = 243
|
||||
Width = 91
|
||||
Height = 25
|
||||
Caption = 'Get Customers'
|
||||
@ -88,10 +88,10 @@ object Form10: TForm10
|
||||
OnClick = edtGetCustomersClick
|
||||
end
|
||||
object DBGrid1: TDBGrid
|
||||
Left = 16
|
||||
Top = 157
|
||||
Width = 497
|
||||
Height = 317
|
||||
Left = 8
|
||||
Top = 272
|
||||
Width = 489
|
||||
Height = 264
|
||||
DataSource = DataSource1
|
||||
TabOrder = 9
|
||||
TitleFont.Charset = DEFAULT_CHARSET
|
||||
@ -100,10 +100,40 @@ object Form10: TForm10
|
||||
TitleFont.Name = 'Tahoma'
|
||||
TitleFont.Style = []
|
||||
end
|
||||
object edtUserName: TEdit
|
||||
Left = 8
|
||||
Top = 104
|
||||
Width = 184
|
||||
Height = 21
|
||||
TabOrder = 10
|
||||
Text = 'dteti'
|
||||
end
|
||||
object btnGetUser: TButton
|
||||
Left = 198
|
||||
Top = 102
|
||||
Width = 91
|
||||
Height = 25
|
||||
Caption = 'Get User'
|
||||
TabOrder = 11
|
||||
OnClick = btnGetUserClick
|
||||
end
|
||||
object lbPerson: TListBox
|
||||
Left = 8
|
||||
Top = 133
|
||||
Width = 489
|
||||
Height = 82
|
||||
Font.Charset = DEFAULT_CHARSET
|
||||
Font.Color = clWindowText
|
||||
Font.Height = -13
|
||||
Font.Name = 'Courier New'
|
||||
Font.Style = []
|
||||
ParentFont = False
|
||||
TabOrder = 12
|
||||
end
|
||||
object DataSource1: TDataSource
|
||||
DataSet = FDMemTable1
|
||||
Left = 264
|
||||
Top = 248
|
||||
Left = 256
|
||||
Top = 280
|
||||
end
|
||||
object FDMemTable1: TFDMemTable
|
||||
FetchOptions.AssignedValues = [evMode]
|
||||
@ -113,8 +143,8 @@ object Form10: TForm10
|
||||
UpdateOptions.AssignedValues = [uvCheckRequired, uvAutoCommitUpdates]
|
||||
UpdateOptions.CheckRequired = False
|
||||
UpdateOptions.AutoCommitUpdates = True
|
||||
Left = 264
|
||||
Top = 312
|
||||
Left = 256
|
||||
Top = 344
|
||||
object FDMemTable1Code: TIntegerField
|
||||
FieldName = 'Code'
|
||||
end
|
||||
|
@ -26,9 +26,13 @@ type
|
||||
FDMemTable1: TFDMemTable;
|
||||
FDMemTable1Code: TIntegerField;
|
||||
FDMemTable1Name: TStringField;
|
||||
edtUserName: TEdit;
|
||||
btnGetUser: TButton;
|
||||
lbPerson: TListBox;
|
||||
procedure btnSubstractClick(Sender: TObject);
|
||||
procedure btnReverseStringClick(Sender: TObject);
|
||||
procedure edtGetCustomersClick(Sender: TObject);
|
||||
procedure btnGetUserClick(Sender: TObject);
|
||||
private
|
||||
{ Private declarations }
|
||||
public
|
||||
@ -82,6 +86,38 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TForm10.btnGetUserClick(Sender: TObject);
|
||||
var
|
||||
lReq: TJSONRPCRequest;
|
||||
lResp: TJSONRPCResponse;
|
||||
lJSON: TJsonObject;
|
||||
begin
|
||||
lbPerson.Clear;
|
||||
lReq := TJSONRPCRequest.Create;
|
||||
try
|
||||
lReq.Method := 'getuser';
|
||||
lReq.ID := Random(1000);
|
||||
lReq.Params.Add(edtUserName.Text);
|
||||
JSONRPCExec('http://localhost:8080/jsonrpc', lReq, lResp);
|
||||
try
|
||||
if Assigned(lResp.Error) then
|
||||
raise Exception.Create(lResp.Error.ErrMessage);
|
||||
|
||||
// Remember that TObject descendants (but TDataset, TJDOJSONObject and TJDOJSONArray)
|
||||
// are serialized as JSON objects
|
||||
lJSON := lResp.Result.AsObject as TJsonObject;
|
||||
lbPerson.Items.Add('First Name:'.PadRight(15) + lJSON.S['firstname']);
|
||||
lbPerson.Items.Add('Last Name:'.PadRight(15) + lJSON.S['lastname']);
|
||||
lbPerson.Items.Add('Married:'.PadRight(15) + lJSON.B['married'].ToString(TUseBoolStrs.True));
|
||||
lbPerson.Items.Add('DOB:'.PadRight(15) + DateToStr(lJSON.D['dob']));
|
||||
finally
|
||||
lResp.Free;
|
||||
end;
|
||||
finally
|
||||
lReq.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TForm10.btnReverseStringClick(Sender: TObject);
|
||||
var
|
||||
lReq: TJSONRPCRequest;
|
||||
|
@ -4,7 +4,7 @@ interface
|
||||
|
||||
uses
|
||||
MVCFramework, MVCFramework.Commons, MVCFramework.JSONRPC, JsonDataObjects,
|
||||
Data.DB;
|
||||
Data.DB, BusinessObjectsU;
|
||||
|
||||
type
|
||||
TMyJSONRPCController = class(TMVCJSONRPCController)
|
||||
@ -12,6 +12,7 @@ type
|
||||
function Subtract(aValue1, aValue2: Integer): Integer;
|
||||
function ReverseString(aString: string): string;
|
||||
function GetCustomers(aString: string): TDataSet;
|
||||
function GetUser(aUserName: string): TPerson;
|
||||
procedure DoSomething;
|
||||
|
||||
end;
|
||||
@ -65,6 +66,15 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMyJSONRPCController.GetUser(aUserName: string): TPerson;
|
||||
begin
|
||||
Result := TPerson.Create;
|
||||
Result.FirstName := 'Daniele (a.k.a. ' + aUserName + ')';
|
||||
Result.LastName := 'Teti';
|
||||
Result.DOB := EncodeDate(1932, 11, 4); // hey, it is a joke :-)
|
||||
Result.Married := True;
|
||||
end;
|
||||
|
||||
function TMyJSONRPCController.ReverseString(aString: string): string;
|
||||
begin
|
||||
Result := System.StrUtils.ReverseString(aString);
|
||||
|
@ -14,7 +14,7 @@ uses
|
||||
MainControllerU in 'MainControllerU.pas',
|
||||
MainWebModuleU in 'MainWebModuleU.pas' {MyWebModule: TWebModule},
|
||||
MVCFramework.JSONRPC in '..\..\sources\MVCFramework.JSONRPC.pas',
|
||||
MVCFramework.Router in '..\..\sources\MVCFramework.Router.pas';
|
||||
BusinessObjectsU in '..\commons\BusinessObjectsU.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
|
@ -143,7 +143,7 @@
|
||||
<DesignClass>TWebModule</DesignClass>
|
||||
</DCCReference>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.JSONRPC.pas"/>
|
||||
<DCCReference Include="..\..\sources\MVCFramework.Router.pas"/>
|
||||
<DCCReference Include="..\commons\BusinessObjectsU.pas"/>
|
||||
<BuildConfiguration Include="Release">
|
||||
<Key>Cfg_2</Key>
|
||||
<CfgParent>Base</CfgParent>
|
||||
|
@ -37,8 +37,6 @@ object MainForm: TMainForm
|
||||
ParentFont = False
|
||||
ReadOnly = True
|
||||
TabOrder = 0
|
||||
ExplicitTop = 49
|
||||
ExplicitWidth = 513
|
||||
end
|
||||
object Memo2: TMemo
|
||||
Left = 0
|
||||
@ -54,9 +52,6 @@ object MainForm: TMainForm
|
||||
ParentFont = False
|
||||
ReadOnly = True
|
||||
TabOrder = 1
|
||||
ExplicitTop = 150
|
||||
ExplicitWidth = 513
|
||||
ExplicitHeight = 229
|
||||
end
|
||||
object Panel1: TPanel
|
||||
Left = 0
|
||||
@ -104,7 +99,6 @@ object MainForm: TMainForm
|
||||
Caption = 'Login'
|
||||
TabOrder = 1
|
||||
OnClick = btnLOGINClick
|
||||
ExplicitHeight = 41
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,4 +1,4 @@
|
||||
// ***************************************************************************
|
||||
// ***************************************************************************
|
||||
//
|
||||
// Delphi MVC Framework
|
||||
//
|
||||
|
@ -4,13 +4,13 @@ object MyDataModule: TMyDataModule
|
||||
Width = 560
|
||||
object FDConnection1: TFDConnection
|
||||
Params.Strings = (
|
||||
'Database=employee'
|
||||
'Database=C:\FB30\examples\empbuild\EMPLOYEE.FDB'
|
||||
'User_Name=sysdba'
|
||||
'Password=masterkey'
|
||||
'Protocol=TCPIP'
|
||||
'Server=localhost'
|
||||
'DriverID=FB')
|
||||
ConnectedStoredUsage = [auDesignTime]
|
||||
ConnectedStoredUsage = []
|
||||
Connected = True
|
||||
LoginPrompt = False
|
||||
Left = 160
|
||||
@ -19,7 +19,6 @@ object MyDataModule: TMyDataModule
|
||||
object qryCustomers: TFDQuery
|
||||
Connection = FDConnection1
|
||||
FetchOptions.AssignedValues = [evUnidirectional]
|
||||
FetchOptions.Unidirectional = True
|
||||
UpdateOptions.AssignedValues = [uvEDelete, uvEInsert, uvEUpdate]
|
||||
UpdateOptions.EnableDelete = False
|
||||
UpdateOptions.EnableInsert = False
|
||||
|
@ -1,4 +1,4 @@
|
||||
// ***************************************************************************
|
||||
// ***************************************************************************
|
||||
//
|
||||
// Delphi MVC Framework
|
||||
//
|
||||
|
@ -79,9 +79,9 @@ begin
|
||||
// Register a custom serializer for TUserRoles (is compatible only with the default serializer)
|
||||
DMVC
|
||||
.Serializers
|
||||
.Items[CreateContentType(TMVCMediaType.APPLICATION_JSON, TMVCCharSet.UTF_8)]
|
||||
.Items[BuildContentType(TMVCMediaType.APPLICATION_JSON, TMVCCharSet.UTF_8)]
|
||||
.RegisterTypeSerializer(TypeInfo(TUserRoles), TUserRolesSerializer.Create);
|
||||
// You can check if this custom type serializer works
|
||||
// You can check how this custom type serializer works
|
||||
// calling http://localhost:8080/customserializationtype
|
||||
end;
|
||||
|
||||
|
@ -1,87 +1,65 @@
|
||||
unit SSEControllerU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework, MVCFramework.Commons;
|
||||
|
||||
type
|
||||
|
||||
[MVCPath('/')]
|
||||
TSSEController = class(TMVCController)
|
||||
public
|
||||
[MVCPath('/stocks')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
[MVCProduces('text/event-stream')]
|
||||
procedure Index;
|
||||
|
||||
protected
|
||||
procedure OnBeforeAction(Context: TWebContext; const AActionName: string;
|
||||
var Handled: Boolean); override;
|
||||
procedure OnAfterAction(Context: TWebContext;
|
||||
const AActionName: string); override;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
MVCFramework.Logger, System.SysUtils, StorageU;
|
||||
|
||||
procedure TSSEController.Index;
|
||||
var
|
||||
lLastEventID: Integer;
|
||||
lCurrentEventID: Integer;
|
||||
lMessage: string;
|
||||
begin
|
||||
// wait a little bit
|
||||
Sleep(1000 + Random(2000));
|
||||
|
||||
// retrieve the last id received by the client reading the request header.
|
||||
lLastEventID := StrToIntDef(Context.Request.Headers['Last-Event-ID'], 0);
|
||||
|
||||
// get the next message to send based on the last id already received by the client
|
||||
lMessage := GetNextDataToSend(lLastEventID, lCurrentEventID);
|
||||
|
||||
// setting up the correct SSE headers
|
||||
ContentType := 'text/event-stream';
|
||||
Context.Response.SetCustomHeader('Cache-Control', 'no-cache');
|
||||
// WARNING!! keep-alive heaer has been set directly on the server!
|
||||
// Context.Response.SetCustomHeader('Connection', 'keep-alive');
|
||||
|
||||
// render the response using SSE compliant data format
|
||||
|
||||
// current event id (the client will resend this number at the next request)
|
||||
ResponseStream.Append('id: ' + IntToStr(lCurrentEventID) + #13);
|
||||
|
||||
// The browser attempts to reconnect to the source roughly 3 seconds after
|
||||
// each connection is closed. You can change that timeout by including a line
|
||||
// beginning with "retry:", followed by the number of milliseconds to wait
|
||||
// before trying to reconnect.
|
||||
ResponseStream.Append('retry: 100'#13);
|
||||
|
||||
ResponseStream.Append('event: stockupdate'#13);
|
||||
|
||||
// actual message
|
||||
ResponseStream.Append('data: ' + lMessage + #13#13);
|
||||
|
||||
// render all the stuff
|
||||
RenderResponseStream;
|
||||
end;
|
||||
|
||||
procedure TSSEController.OnAfterAction(Context: TWebContext;
|
||||
const AActionName: string);
|
||||
begin
|
||||
{ Executed after each action }
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TSSEController.OnBeforeAction(Context: TWebContext;
|
||||
const AActionName: string; var Handled: Boolean);
|
||||
begin
|
||||
{ Executed before each action
|
||||
if handled is true (or an exception is raised) the actual
|
||||
action will not be called }
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework, MVCFramework.Commons;
|
||||
|
||||
type
|
||||
|
||||
[MVCPath('/')]
|
||||
TSSEController = class(TMVCController)
|
||||
public
|
||||
[MVCPath('/stocks')]
|
||||
[MVCHTTPMethod([httpGET])]
|
||||
[MVCProduces('text/event-stream')]
|
||||
procedure Index;
|
||||
|
||||
protected
|
||||
procedure OnBeforeAction(Context: TWebContext; const AActionName: string;
|
||||
var Handled: Boolean); override;
|
||||
procedure OnAfterAction(Context: TWebContext;
|
||||
const AActionName: string); override;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
MVCFramework.Logger, System.SysUtils, StorageU;
|
||||
|
||||
procedure TSSEController.Index;
|
||||
var
|
||||
lLastEventID: Integer;
|
||||
lCurrentEventID: Integer;
|
||||
lMessage: string;
|
||||
begin
|
||||
// wait a little bit
|
||||
Sleep(1000 + Random(2000));
|
||||
|
||||
// retrieve the last id received by the client reading the request header.
|
||||
lLastEventID := StrToIntDef(Context.Request.Headers[TMVCConstants.SSE_LAST_EVENT_ID], 0);
|
||||
|
||||
// get the next message to send based on the last id already received by the client
|
||||
lMessage := GetNextDataToSend(lLastEventID, lCurrentEventID);
|
||||
|
||||
RenderSSE(lCurrentEventID.ToString, lMessage, 'stockupdate');
|
||||
end;
|
||||
|
||||
procedure TSSEController.OnAfterAction(Context: TWebContext;
|
||||
const AActionName: string);
|
||||
begin
|
||||
{ Executed after each action }
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TSSEController.OnBeforeAction(Context: TWebContext;
|
||||
const AActionName: string; var Handled: Boolean);
|
||||
begin
|
||||
{ Executed before each action
|
||||
if handled is true (or an exception is raised) the actual
|
||||
action will not be called }
|
||||
inherited;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -6,13 +6,7 @@ uses
|
||||
System.SysUtils,
|
||||
MVCFramework.Logger,
|
||||
MVCFramework.Commons,
|
||||
|
||||
{$IFNDEF LINUX}
|
||||
|
||||
ReqMulti,
|
||||
|
||||
{$ENDIF}
|
||||
|
||||
Web.WebReq,
|
||||
Web.WebBroker,
|
||||
IdHTTPWebBrokerBridge,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{FD2E10E8-0B57-424F-BB4B-827E8087AC33}</ProjectGuid>
|
||||
<ProjectGuid>{56928A09-5B7B-4920-ABAA-CB68F0AC2958}</ProjectGuid>
|
||||
<ProjectVersion>18.2</ProjectVersion>
|
||||
<FrameworkType>VCL</FrameworkType>
|
||||
<MainSource>SSESample.dpr</MainSource>
|
||||
|
@ -31,7 +31,7 @@ uses
|
||||
MVCFramework.Commons, System.IOUtils;
|
||||
|
||||
type
|
||||
{ This class implements the mustache view engine for server side views }
|
||||
{ This class implements the TemplatePro view engine for server side views }
|
||||
TMVCTemplateProViewEngine = class(TMVCBaseViewEngine)
|
||||
public
|
||||
procedure Execute(const ViewName: string); override;
|
||||
@ -95,21 +95,4 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
// procedure TMVCTemplateProViewEngine.Execute(const ViewName: string);
|
||||
// var
|
||||
// ViewFileName: string;
|
||||
// ViewTemplate: RawUTF8;
|
||||
// ViewEngine: TSynMustache;
|
||||
// begin
|
||||
// PrepareModels;
|
||||
// ViewFileName := GetRealFileName(ViewName);
|
||||
// if not FileExists(ViewFileName) then
|
||||
// raise EMVCFrameworkViewException.CreateFmt('View [%s] not found', [ViewName]);
|
||||
// ViewTemplate := StringToUTF8(TFile.ReadAllText(ViewFileName, TEncoding.UTF8));
|
||||
// ViewEngine := TSynMustache.Parse(ViewTemplate);
|
||||
// SetOutput(UTF8Tostring(ViewEngine.RenderJSON(FJSONModel)));
|
||||
// end;
|
||||
|
||||
{$WARNINGS ON}
|
||||
|
||||
end.
|
||||
|
@ -49,14 +49,14 @@ type
|
||||
TMVCCache = class sealed
|
||||
private
|
||||
FStorage: TObjectDictionary<string, TMVCCacheItem>;
|
||||
FCriticalSection: TCriticalSection;
|
||||
FMREW: TMultiReadExclusiveWriteSynchronizer;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure SetValue(const AName: string; const AValue: TValue);
|
||||
function Contains(const AName: string; out AValue: TValue): Boolean;
|
||||
function GetValue(const AName: string): TValue;
|
||||
function ExecOnItem(const AName: string; const AAction: TProc<TValue>): Boolean;
|
||||
function ExecOnItemWithWriteLock(const AName: string; const AAction: TProc<TValue>): Boolean;
|
||||
end;
|
||||
|
||||
TMVCCacheSingleton = class
|
||||
@ -84,7 +84,7 @@ var
|
||||
begin
|
||||
lValue := AValue;
|
||||
|
||||
FCriticalSection.DoWithLock(
|
||||
FMREW.DoWithWriteLock(
|
||||
procedure
|
||||
var
|
||||
lItem: TMVCCacheItem;
|
||||
@ -107,12 +107,12 @@ begin
|
||||
end);
|
||||
end;
|
||||
|
||||
function TMVCCache.ExecOnItem(const AName: string; const AAction: TProc<TValue>): Boolean;
|
||||
function TMVCCache.ExecOnItemWithWriteLock(const AName: string; const AAction: TProc<TValue>): Boolean;
|
||||
var
|
||||
lItem: TMVCCacheItem;
|
||||
begin
|
||||
Result := False;
|
||||
FCriticalSection.Enter;
|
||||
FMREW.BeginWrite;
|
||||
try
|
||||
if FStorage.TryGetValue(AName, lItem) then
|
||||
begin
|
||||
@ -120,7 +120,7 @@ begin
|
||||
AAction(lItem.Value);
|
||||
end;
|
||||
finally
|
||||
FCriticalSection.Leave;
|
||||
FMREW.EndWrite;
|
||||
end;
|
||||
end;
|
||||
|
||||
@ -129,7 +129,7 @@ var
|
||||
lValue: TMVCCacheItem;
|
||||
lRes: Boolean;
|
||||
begin
|
||||
FCriticalSection.DoWithLock(
|
||||
FMREW.DoWithReadLock(
|
||||
procedure
|
||||
begin
|
||||
lRes := FStorage.TryGetValue(AName, lValue);
|
||||
@ -143,12 +143,12 @@ constructor TMVCCache.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
FStorage := TObjectDictionary<string, TMVCCacheItem>.Create([doOwnsValues]);
|
||||
FCriticalSection := TCriticalSection.Create;
|
||||
FMREW := TMultiReadExclusiveWriteSynchronizer.Create;
|
||||
end;
|
||||
|
||||
destructor TMVCCache.Destroy;
|
||||
begin
|
||||
FCriticalSection.Free;
|
||||
FMREW.Free;
|
||||
FStorage.Free;
|
||||
inherited;
|
||||
end;
|
||||
@ -159,7 +159,7 @@ var
|
||||
lResult: TValue;
|
||||
begin
|
||||
Result := TValue.Empty;
|
||||
FCriticalSection.DoWithLock(
|
||||
FMREW.DoWithReadLock(
|
||||
procedure
|
||||
begin
|
||||
if FStorage.TryGetValue(AName, lItem) then
|
||||
|
@ -103,6 +103,8 @@ type
|
||||
DEFAULT_CONTENT_TYPE = TMVCMediaType.APPLICATION_JSON;
|
||||
CURRENT_USER_SESSION_KEY = '__DMVC_CURRENT_USER__';
|
||||
LAST_AUTHORIZATION_HEADER_VALUE = '__DMVC_LAST_AUTHORIZATION_HEADER_VALUE_';
|
||||
SSE_RETRY_DEFAULT = 100;
|
||||
SSE_LAST_EVENT_ID = 'Last-Event-ID';
|
||||
end;
|
||||
|
||||
TMVCConfigKey = record
|
||||
@ -322,6 +324,10 @@ type
|
||||
{ public declarations }
|
||||
end;
|
||||
|
||||
EMVCViewError = class(EMVCException)
|
||||
|
||||
end;
|
||||
|
||||
TMVCRequestParamsTable = class(TDictionary<string, string>)
|
||||
private
|
||||
{ private declarations }
|
||||
@ -366,16 +372,23 @@ type
|
||||
constructor Create;
|
||||
end;
|
||||
|
||||
TMVCCriticalSectionHelper = class helper
|
||||
for TCriticalSection
|
||||
TMVCCriticalSectionHelper = class helper for TCriticalSection
|
||||
public
|
||||
procedure DoWithLock(const AAction: TProc);
|
||||
function DoWithLockTimeout(const AAction: TProc; const ATimeOut: UInt32): TWaitResult;
|
||||
end;
|
||||
|
||||
TMultiReadExclusiveWriteSynchronizerHelper = class helper
|
||||
for TMultiReadExclusiveWriteSynchronizer
|
||||
public
|
||||
procedure DoWithWriteLock(const AAction: TProc);
|
||||
procedure DoWithReadLock(const AAction: TProc);
|
||||
end;
|
||||
|
||||
TMVCConfig = class sealed
|
||||
private
|
||||
FConfig: TDictionary<string, string>;
|
||||
|
||||
function GetValue(const AIndex: string): string;
|
||||
function GetValueAsInt64(const AIndex: string): Int64;
|
||||
procedure SetValue(const AIndex: string; const AValue: string);
|
||||
@ -419,7 +432,7 @@ function ByteToHex(AInByte: Byte): string;
|
||||
function BytesToHex(ABytes: TBytes): string;
|
||||
|
||||
procedure SplitContentMediaTypeAndCharset(const aContentType: string; var aContentMediaType: string; var aContentCharSet: string);
|
||||
function CreateContentType(const aContentMediaType: string; const aContentCharSet: string): string;
|
||||
function BuildContentType(const aContentMediaType: string; const aContentCharSet: string): string;
|
||||
|
||||
const
|
||||
MVC_HTTP_METHODS_WITHOUT_CONTENT: TMVCHTTPMethods = [httpGET, httpDELETE, httpHEAD, httpOPTIONS];
|
||||
@ -514,7 +527,7 @@ begin
|
||||
Result := Result + ByteToHex(B);
|
||||
end;
|
||||
|
||||
function CreateContentType(const aContentMediaType: string; const aContentCharSet: string): string;
|
||||
function BuildContentType(const aContentMediaType: string; const aContentCharSet: string): string;
|
||||
begin
|
||||
if aContentCharSet = '' then
|
||||
begin
|
||||
@ -524,7 +537,7 @@ begin
|
||||
begin
|
||||
Result := aContentMediaType + ';charset=' + aContentCharSet;
|
||||
end;
|
||||
Result := Result.ToLower.Replace(' ','',[rfReplaceAll]);
|
||||
Result := Result.ToLower.Replace(' ', '', [rfReplaceAll]);
|
||||
end;
|
||||
|
||||
procedure SplitContentMediaTypeAndCharset(const aContentType: string; var aContentMediaType: string; var aContentCharSet: string);
|
||||
@ -874,6 +887,30 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
{ TMultiReadExclusiveWriteSynchronizerHelper }
|
||||
|
||||
procedure TMultiReadExclusiveWriteSynchronizerHelper.DoWithReadLock(
|
||||
const AAction: TProc);
|
||||
begin
|
||||
Self.BeginRead;
|
||||
try
|
||||
AAction();
|
||||
finally
|
||||
Self.EndRead;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMultiReadExclusiveWriteSynchronizerHelper.DoWithWriteLock(
|
||||
const AAction: TProc);
|
||||
begin
|
||||
Self.BeginWrite;
|
||||
try
|
||||
AAction();
|
||||
finally
|
||||
Self.EndWrite;
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
Lock := TObject.Create;
|
||||
|
@ -519,6 +519,7 @@ var
|
||||
lJAlg: TJSONString;
|
||||
lAlgName: string;
|
||||
begin
|
||||
Result := False;
|
||||
Error := '';
|
||||
lPieces := Token.Split(['.']);
|
||||
if Length(lPieces) <> 3 then
|
||||
|
@ -767,7 +767,7 @@ begin
|
||||
FQueryStringParams := nil;
|
||||
FRawBody := nil;
|
||||
FAccept := 'application/json';
|
||||
FContentType := CreateContentType(TMVCMediaType.APPLICATION_JSON, TMVCCharset.UTF_8);
|
||||
FContentType := BuildContentType(TMVCMediaType.APPLICATION_JSON, TMVCCharset.UTF_8);
|
||||
FResource := '';
|
||||
FContentEncoding := '';
|
||||
FRequestHeaders := TStringlist.Create;
|
||||
@ -1433,7 +1433,6 @@ function TRESTClient.SendHTTPCommandWithBody(const ACommand: TMVCHTTPMethodType;
|
||||
var
|
||||
lBytes: TArray<Byte>;
|
||||
lContentCharset: string;
|
||||
lContentType: string;
|
||||
lEncoding: TEncoding;
|
||||
begin
|
||||
Result := TRESTResponse.Create;
|
||||
@ -1445,18 +1444,11 @@ begin
|
||||
lContentCharset := 'UTF-8';
|
||||
if AContentCharset <> '' then
|
||||
lContentCharset := AContentCharset;
|
||||
lContentType := CreateContentType(AContentMediaType, lContentCharset);
|
||||
|
||||
// FHTTP.Request.ContentType := lContentTypeWithCharset;
|
||||
FHTTP.Request.ContentType := lContentType;
|
||||
// FHTTP.Request.CharSet := AContentCharset;
|
||||
// FHTTP.Request.ContentEncoding := AContentCharset;
|
||||
FHTTP.Request.ContentType := BuildContentType(AContentMediaType, lContentCharset);
|
||||
|
||||
HandleRequestCookies;
|
||||
try
|
||||
// if FHTTP.Request.CharSet = '' then
|
||||
// FHTTP.Request.CharSet := 'utf-8';
|
||||
|
||||
case ACommand of
|
||||
httpGET:
|
||||
begin
|
||||
@ -1492,22 +1484,6 @@ begin
|
||||
('Sorry, PATCH is not supported by the RESTClient because is not supportd by the TidHTTP');
|
||||
end;
|
||||
|
||||
// httpPUT:
|
||||
// begin
|
||||
// RawBody.Position := 0;
|
||||
// RawBody.Size := 0;
|
||||
// lEncoding := TEncoding.GetEncoding(FHTTP.Request.CharSet);
|
||||
// try
|
||||
// lBytes := TEncoding.Convert(TEncoding.Default, lEncoding,
|
||||
// TEncoding.Default.GetBytes(ABody));
|
||||
// RawBody.WriteData(lBytes, Length(lBytes));
|
||||
// finally
|
||||
// lEncoding.Free;
|
||||
// end;
|
||||
//
|
||||
// FHTTP.Put(AResource, RawBody, Result.Body);
|
||||
// end;
|
||||
|
||||
httpDELETE:
|
||||
begin
|
||||
FHTTP.Delete(AResource);
|
||||
|
@ -467,13 +467,16 @@ type
|
||||
procedure Render(const AErrorCode: Integer; const AErrorMessage: string; const AErrorClassName: string = ''); overload;
|
||||
procedure Render(const AException: Exception; AExceptionItems: TList<string> = nil; const AOwns: Boolean = True); overload;
|
||||
procedure Render(const AError: TMVCErrorResponse; const AOwns: Boolean = True); overload;
|
||||
|
||||
// SSE Support
|
||||
procedure RenderSSE(const EventID: string; const EventData: string; EventName: string = ''; const Retry: Integer = TMVCConstants.SSE_RETRY_DEFAULT);
|
||||
// Properties
|
||||
property Context: TWebContext read GetContext write FContext;
|
||||
property Session: TWebSession read GetSession;
|
||||
property ContentType: string read GetContentType write SetContentType;
|
||||
property StatusCode: Integer read GetStatusCode write SetStatusCode;
|
||||
property ViewModel: TMVCViewDataObject read GetViewModel;
|
||||
property ViewDataSets: TObjectDictionary<string, TDataSet> read GetViewDataSets;
|
||||
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
@ -713,6 +716,8 @@ type
|
||||
property Output: string read FOutput;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
function IsShuttingDown: Boolean;
|
||||
procedure EnterInShutdownState;
|
||||
|
||||
@ -988,7 +993,7 @@ end;
|
||||
procedure TMVCWebRequest.DefineContentType;
|
||||
begin
|
||||
SplitContentMediaTypeAndCharset(FWebRequest.GetFieldByName('Content-Type'), FContentMediaType, FCharset);
|
||||
FContentType := CreateContentType(FContentMediaType, FCharset);
|
||||
FContentType := BuildContentType(FContentMediaType, FCharset);
|
||||
end;
|
||||
|
||||
destructor TMVCWebRequest.Destroy;
|
||||
@ -1708,7 +1713,7 @@ begin
|
||||
LSelectedController.MVCControllerAfterCreate;
|
||||
try
|
||||
LHandled := False;
|
||||
LSelectedController.ContentType := CreateContentType(LResponseContentMediaType, LResponseContentCharset);
|
||||
LSelectedController.ContentType := BuildContentType(LResponseContentMediaType, LResponseContentCharset);
|
||||
// LSelectedController.ContentCharset := LResponseContentCharset;
|
||||
if not LHandled then
|
||||
begin
|
||||
@ -2058,7 +2063,7 @@ procedure TMVCEngine.RegisterDefaultsSerializers;
|
||||
var
|
||||
lDefaultSerializerContentType: string;
|
||||
begin
|
||||
lDefaultSerializerContentType := CreateContentType(TMVCMediaType.APPLICATION_JSON, TMVCCharset.UTF_8);
|
||||
lDefaultSerializerContentType := BuildContentType(TMVCMediaType.APPLICATION_JSON, TMVCCharset.UTF_8);
|
||||
if not FSerializers.ContainsKey(lDefaultSerializerContentType) then
|
||||
begin
|
||||
FSerializers.Add(
|
||||
@ -2067,7 +2072,7 @@ begin
|
||||
end;
|
||||
|
||||
// register the same serializer without the charset in the contenttype
|
||||
lDefaultSerializerContentType := CreateContentType(TMVCMediaType.APPLICATION_JSON, '');
|
||||
lDefaultSerializerContentType := BuildContentType(TMVCMediaType.APPLICATION_JSON, '');
|
||||
if not FSerializers.ContainsKey(lDefaultSerializerContentType) then
|
||||
begin
|
||||
FSerializers.Add(
|
||||
@ -2126,9 +2131,9 @@ begin
|
||||
if TFile.Exists(AFileName) then
|
||||
begin
|
||||
if FMediaTypes.TryGetValue(LowerCase(ExtractFileExt(AFileName)), LContentType) then
|
||||
LContentType := CreateContentType(lContentType, FConfig[TMVCConfigKey.DefaultContentCharset])
|
||||
LContentType := BuildContentType(lContentType, FConfig[TMVCConfigKey.DefaultContentCharset])
|
||||
else
|
||||
LContentType := CreateContentType(TMVCMediaType.APPLICATION_OCTETSTREAM, '');
|
||||
LContentType := BuildContentType(TMVCMediaType.APPLICATION_OCTETSTREAM, '');
|
||||
TMVCStaticContents.SendFile(AFileName, LContentType, AContext);
|
||||
Result := True;
|
||||
end;
|
||||
@ -2411,7 +2416,7 @@ begin
|
||||
lCharset := TMVCConstants.DEFAULT_CONTENT_CHARSET;
|
||||
if LContentType.IsEmpty then
|
||||
LContentType := TMVCConstants.DEFAULT_CONTENT_TYPE;
|
||||
lContentType := CreateContentType(LContentType, lCharset);
|
||||
lContentType := BuildContentType(LContentType, lCharset);
|
||||
|
||||
LOutEncoding := TEncoding.GetEncoding(lCharset);
|
||||
try
|
||||
@ -2648,6 +2653,41 @@ begin
|
||||
Render(ResponseStream.ToString);
|
||||
end;
|
||||
|
||||
procedure TMVCController.RenderSSE(const EventID, EventData: string;
|
||||
EventName: string; const Retry: Integer);
|
||||
begin
|
||||
// setting up the correct SSE headers
|
||||
ContentType := 'text/event-stream';
|
||||
Context.Response.SetCustomHeader('Cache-Control', 'no-cache');
|
||||
Context.Response.StatusCode := HTTP_STATUS.OK;
|
||||
|
||||
// render the response using SSE compliant data format
|
||||
|
||||
// current event id (the client will resend this number at the next request)
|
||||
ResponseStream.Append(Format('id: %s'#13, [EventID]));
|
||||
|
||||
// The browser attempts to reconnect to the source roughly 3 seconds after
|
||||
// each connection is closed. You can change that timeout by including a line
|
||||
// beginning with "retry:", followed by the number of milliseconds to wait
|
||||
// before trying to reconnect.
|
||||
|
||||
if Retry > -1 then
|
||||
begin
|
||||
ResponseStream.Append(Format('retry: %d'#13, [Retry]));
|
||||
end;
|
||||
|
||||
if not EventName.IsEmpty then
|
||||
begin
|
||||
ResponseStream.Append(Format('event: %s'#13, [EventName]));
|
||||
end;
|
||||
|
||||
// actual message
|
||||
ResponseStream.Append('data: ' + EventData + #13#13);
|
||||
|
||||
// render all the stuff
|
||||
RenderResponseStream;
|
||||
end;
|
||||
|
||||
procedure TMVCController.Render(const ACollection: IMVCList);
|
||||
begin
|
||||
Render(ACollection, stDefault);
|
||||
|
Loading…
Reference in New Issue
Block a user