Improved JSON RPC 2.0 Client Sample

This commit is contained in:
Daniele Teti 2017-10-09 16:17:12 +02:00
parent df0479d251
commit ac654658f1
26 changed files with 313 additions and 219 deletions

27
.gitignore vendored
View File

@ -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~

View File

@ -201,6 +201,7 @@ resourcestring
sCRUDMethodsIntf =
sLineBreak +
' public' + sLineBreak +
' //Sample CRUD Actions for a "Customer" entity' + sLineBreak +
' [MVCPath(''/customers'')]' + sLineBreak +
' [MVCHTTPMethod([httpGET])]' + sLineBreak +

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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.

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -1,4 +1,4 @@
// ***************************************************************************
// ***************************************************************************
//
// Delphi MVC Framework
//

View File

@ -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

View File

@ -1,4 +1,4 @@
// ***************************************************************************
// ***************************************************************************
//
// Delphi MVC Framework
//

View File

@ -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;

View File

@ -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.

View File

@ -6,13 +6,7 @@ uses
System.SysUtils,
MVCFramework.Logger,
MVCFramework.Commons,
{$IFNDEF LINUX}
ReqMulti,
{$ENDIF}
Web.WebReq,
Web.WebBroker,
IdHTTPWebBrokerBridge,

View File

@ -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>

View File

@ -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.

View File

@ -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

View File

@ -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;

View File

@ -519,6 +519,7 @@ var
lJAlg: TJSONString;
lAlgName: string;
begin
Result := False;
Error := '';
lPieces := Token.Split(['.']);
if Length(lPieces) <> 3 then

View File

@ -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);

View File

@ -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);