-
-
Notifications
You must be signed in to change notification settings - Fork 135
/
Copy pathmormot.soa.core.pas
2011 lines (1860 loc) · 79 KB
/
mormot.soa.core.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// Interface-based SOA Process Core Types and Classes
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.soa.core;
{
*****************************************************************************
Shared Interface-based Service Oriented Architecture (SOA) Process
- TOrmServiceLog TOrmServiceNotifications Classes
- TServiceFactory Abstract Service Provider
- TServiceFactoryServerAbstract Abstract Service Provider
- TServiceContainer Abstract Services Holder
- SOA Related Interfaces
- TServicesPublishedInterfacesList Services Catalog
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
uses
sysutils,
classes,
variants,
mormot.core.base,
mormot.core.os,
mormot.core.buffers,
mormot.core.unicode,
mormot.core.text,
mormot.core.variants,
mormot.core.data,
mormot.core.rtti,
mormot.core.json,
mormot.core.threads,
mormot.core.interfaces,
mormot.orm.base,
mormot.orm.core;
{ ************ TOrmServiceLog TOrmServiceNotifications Classes }
type
/// common ancestor for storing interface-based service execution statistics
// - each call could be logged and monitored in the database
// - TServiceMethodExecute could store all its calls in such a table
// - enabled on server side via either TServiceFactoryServer.SetServiceLog or
// TServiceContainerServer.SetServiceLog method
TOrmServiceLog = class(TOrmNoCaseExtended)
protected
fMethod: RawUtf8;
fInput: variant;
fOutput: variant;
fUser: integer;
fSession: integer;
fTime: TModTime;
fMicroSec: integer;
fIP: RawUtf8;
public
/// overriden method creating an index on the Method/MicroSec columns
class procedure InitializeTable(const Server: IRestOrmServer;
const FieldName: RawUtf8; Options: TOrmInitializeTableOptions); override;
published
/// the 'interface.method' identifier of this call
// - this column will be indexed, for fast SQL queries, with the MicroSec
// column (for performance tuning)
property Method: RawUtf8
read fMethod write fMethod;
/// the input parameters, as a JSON document
// - will be stored in JSON_FAST_EXTENDED format, i.e. with
// shortened field names, for smaller TEXT storage
// - content may be searched using JsonGet/JsonHas SQL functions on a
// SQlite3 storage, or with direct document query under MongoDB/PostgreSQL
property Input: variant
read fInput write fInput;
/// the output parameters, as a JSON document, including result: for a function
// - will be stored in JSON_FAST_EXTENDED format, i.e. with
// shortened field names, for smaller TEXT storage
// - content may be searched using JsonGet/JsonHas SQL functions on a
// SQlite3 storage, or with direct document query under MongoDB/PostgreSQL
property Output: variant
read fOutput write fOutput;
/// the Session ID, if there is any
property Session: integer
read fSession write fSession;
/// the User ID, if there is an identified Session
property User: integer
read fUser write fUser;
/// will be filled by the ORM when this record is written in the database
property Time: TModTime
read fTime write fTime;
/// execution time of this method, in micro seconds
property MicroSec: integer
read fMicroSec write fMicroSec;
/// if not localhost/127.0.0.1, the remote IP address
property IP: RawUtf8
read fIP write fIP;
end;
/// execution statistics used for DB-based asynchronous notifications
// - as used by TServiceFactoryClient.SendNotifications
// - here, the Output column may contain the information about an error
// occurred during process
TOrmServiceNotifications = class(TOrmServiceLog)
protected
fSent: TTimeLog;
public
/// this overriden method will create an index on the 'Sent' column
class procedure InitializeTable(const Server: IRestOrmServer;
const FieldName: RawUtf8; Options: TOrmInitializeTableOptions); override;
/// search for pending events since a supplied ID
// - returns FALSE if no notification was found
// - returns TRUE ad fill a TDocVariant array of JSON Objects, including
// "ID": field, and Method as "MethodName": field
class function LastEventsAsObjects(const Rest: IRestOrm; LastKnownID: TID;
Limit: integer; Service: TInterfaceFactory; out Dest: TDocVariantData;
const MethodName: RawUtf8 = 'Method'; IDAsHexa: boolean = false): boolean;
/// allows to convert the Input array into a proper single JSON Object
// - "ID": field will be included, and Method as "MethodName": field
function SaveInputAsObject(Service: TInterfaceFactory;
const MethodName: RawUtf8 = 'Method'; IDAsHexa: boolean = false): variant; virtual;
/// run FillOne and SaveInputAsObject into a TDocVariant array of JSON Objects
// - "ID": field will be included, and Method as "MethodName": field
procedure SaveFillInputsAsObjects(Service: TInterfaceFactory;
out Dest: TDocVariantData; const MethodName: RawUtf8 = 'Method';
IDAsHexa: boolean = false);
published
/// when this notification has been sent
// - equals 0 until it was actually notified
property Sent: TTimeLog
read fSent write fSent;
end;
/// class-reference type (metaclass) for storing interface-based service
// execution statistics
// - you could inherit from TOrmServiceLog, and specify additional
// fields corresponding to the execution context
TOrmServiceLogClass = class of TOrmServiceLog;
/// class-reference type (metaclass) for storing interface-based service
// execution statistics used for DB-based asynchronous notifications
// - as used by TServiceFactoryClient.SendNotifications
TOrmServiceNotificationsClass = class of TOrmServiceNotifications;
{ ************ TServiceFactory Abstract Service Provider }
type
/// exception dedicated to interface based service implementation
EServiceException = class(ESynException);
/// the possible Server-side instance implementation patterns for
// interface-based services
// - each interface-based service will be implemented by a corresponding
// class instance on the server: this parameter is used to define how
// class instances are created and managed
// - on the Client-side, each instance will be handled depending on the
// server side implementation (i.e. with sicClientDriven behavior if necessary)
// - sicSingle: one object instance is created per call - this is the
// most expensive way of implementing the service, but is safe for simple
// workflows (like a one-type call); this is the default setting for
// TRestServer.ServiceRegister method
// - sicShared: one object instance is used for all incoming calls and is
// not recycled subsequent to the calls - the implementation should be
// thread-safe on the server side
// - sicClientDriven: one object instance will be created in synchronization
// with the client-side lifetime of the corresponding interface: when the
// interface will be released on client, it will be released on the server
// side - a numerical identifier will be transmitted for all JSON requests
// - sicPerSession, sicPerUser and sicPerGroup modes will maintain one
// object instance per running session / user / group (only working if
// RESTful authentication is enabled) - since it may be shared among users or
// groups, the sicPerUser and sicPerGroup implementation should be thread-safe
// - sicPerThread will maintain one object instance per calling thread - it
// may be useful instead of sicShared mode if the service process expects
// some per-heavy thread initialization, for instance
TServiceInstanceImplementation = (
sicSingle,
sicShared,
sicClientDriven,
sicPerSession,
sicPerUser,
sicPerGroup,
sicPerThread);
/// set of Server-side instance implementation patterns for
// interface-based services
TServiceInstanceImplementations = set of TServiceInstanceImplementation;
/// how TServiceFactoryServer.SetOptions() will set the options value
TServiceMethodOptionsAction = (
moaReplace,
moaInclude,
moaExclude);
/// used by TServiceAuthorization to stored its authorizations
TServiceAuthorizationState = (
idAllowAll,
idDenyAll,
idAllowed,
idDenied);
/// used by TServiceFactoryExecution to store its authorizations
{$ifdef USERECORDWITHMETHODS}
TServiceAuthorization = record
{$else}
TServiceAuthorization = object
{$endif USERECORDWITHMETHODS}
/// set if all TAuthGroup ID(s) should be defined for this factory
// - used on server side within TRestServerUriContext.ExecuteSoaByInterface
// - idAllowed, idDenied define what ID[] are storing
// - default is idAllowAll
StateID: TServiceAuthorizationState;
/// the sorted list of allowed/denied TAuthGroup ID(s)
// - used on server side within TRestServerUriContext.ExecuteSoaByInterface
// - IDs should be in 32-bit range, to reduce memory/cache size
// - idAllowed, idDenied define what ID[] are storing
SortedID: TIntegerDynArray;
/// quickly check if this TAuthGroup ID can execute this method
function IsDenied(const ID: TID): boolean;
/// define idAllowAll for this method, and remove any previous SortedID
procedure AllowAll;
/// define idDenyAll for this method, and remove any previous SortedID
procedure DenyAll;
/// deny one TAuthGroup ID for this method, likely to use idDenied state
// - can also remove a previous Allow(ID) during idAllowed state
procedure Deny(const ID: TID);
/// allow one TAuthGroup ID for this method, likely to use idAllowed state
// - can also remove a previous Deny(ID) during idDenied state
procedure Allow(const ID: TID);
end;
/// internal per-method list of execution context as hold in TServiceFactory
TServiceFactoryExecution = record
/// execution options for this method (about thread safety or logging)
Options: TInterfaceMethodOptions;
/// store the current defined authorization of this method
Auth: TServiceAuthorization;
/// where execution information should be written as TOrmServiceLog
// - is a weak pointer to a IRestOrm instance to avoid reference counting
LogRest: pointer;
/// the TOrmServiceLog class to use, as defined in LogRest.Model
LogClass: TOrmServiceLogClass;
end;
/// points to the execution context of one method within TServiceFactory
PServiceFactoryExecution = ^TServiceFactoryExecution;
/// several method execution execution contexts
TServiceFactoryExecutionDynArray = array of TServiceFactoryExecution;
/// an abstract service provider, as registered in TServiceContainer
// - each registered interface has its own TServiceFactory instance, available
// as one TServiceContainer item from TRest.Services property
// - this will be either implemented by a registered TInterfacedObject on the
// server, or by a on-the-fly generated fake TInterfacedObject class
// communicating via JSON on a client
// - TRestServer will have to register an interface implementation as:
// ! Server.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared);
// - TRestClientUri will have to register an interface remote access as:
// ! Client.ServiceRegister([TypeInfo(ICalculator)],sicShared));
// note that the implementation (TServiceCalculator) remain on the server side
// only: the client only needs the ICalculator interface
// - then TRestServer and TRestClientUri will both have access to the
// service, via their Services property, e.g. as:
// !var
// ! I: ICalculator;
// !...
// ! if Services.Info(ICalculator).Get(I) then
// ! result := I.Add(10,20);
// which is in practice to be used with the faster wrapper method:
// ! if Services.Resolve(ICalculator,I) then
// ! result := I.Add(10,20);
TServiceFactory = class(TInjectableObject)
protected
fInterface: TInterfaceFactory;
fInterfaceUri: RawUtf8;
fInterfaceMangledUri: RawUtf8;
fInstanceCreation: TServiceInstanceImplementation;
fOrm: IRestOrm;
fSharedInstance: TInterfacedObject;
fContract: RawUtf8;
fContractHash: RawUtf8;
fContractExpected: RawUtf8;
fExecution: TServiceFactoryExecutionDynArray;
/// union of all fExecution[].Options
fAnyOptions: TInterfaceMethodOptions;
procedure ExecutionAction(const aMethod: array of RawUtf8;
aOptions: TInterfaceMethodOptions; aAction: TServiceMethodOptionsAction);
function GetInterfaceTypeInfo: PRttiInfo;
{$ifdef HASINLINE}inline;{$endif}
function GetInterfaceIID: TGuid;
{$ifdef HASINLINE}inline;{$endif}
public
/// initialize the service provider parameters
// - it will check and retrieve all methods of the supplied interface,
// and prepare all internal structures for its serialized execution
// - supplied TInterfaceResolver should be able to resolve IRestOrm,
// and is typically a TRest instance
constructor Create(aOwner: TInterfaceResolver; aInterface: PRttiInfo;
aInstanceCreation: TServiceInstanceImplementation;
const aContractExpected: RawUtf8); reintroduce;
/// retrieve an instance of this interface
// - this virtual method will be overridden to reflect the expected
// behavior of client or server side
// - can be used as such to resolve an I: ICalculator interface:
// ! var I: ICalculator;
// ! begin
// ! if fClient.Services.Info(TypeInfo(ICalculator)).Get(I) then
// ! ... use I
function Get(out Obj): boolean; virtual; abstract;
/// retrieve the published signature of this interface
// - is always available on TServiceFactoryServer, but TServiceFactoryClient
// will be able to retrieve it only if TServiceContainerServer.PublishSignature
// is set to TRUE (which is not the default setting, for security reasons)
function RetrieveSignature: RawUtf8; virtual; abstract;
/// search for a method name within this Interface RTTI and pseudo-methods
// - will return -1 if not found, im* pseudo-methods as 0..3, or the index
// in InterfaceFactory.Methods[] incremented by SERVICE_PSEUDO_METHOD_COUNT
function ServiceMethodIndex(const Name: RawUtf8): PtrInt;
/// access to the registered Interface RTTI information
property InterfaceFactory: TInterfaceFactory
read fInterface;
/// the registered Interface low-level Delphi RTTI type
// - just maps InterfaceFactory.InterfaceTypeInfo
property InterfaceTypeInfo: PRttiInfo
read GetInterfaceTypeInfo;
/// the registered Interface GUID
// - just maps InterfaceFactory.InterfaceIID
property InterfaceIID: TGuid
read GetInterfaceIID;
/// the service contract, serialized as a JSON object
// - a "contract" is in fact the used interface signature, i.e. its
// implementation mode (InstanceCreation) and all its methods definitions
// - a possible value for a one-method interface defined as such:
// ! function ICalculator.Add(n1,n2: integer): integer;
// may be returned as the following JSON object:
// $ {"contract":"Calculator","implementation":"shared",
// $ "methods":[{"method":"Add",
// $ "arguments":[{"argument":"self","direction":"in","type":"self"},
// $ {"argument":"n1","direction":"in","type":"integer"},
// $ {"argument":"n2","direction":"in","type":"integer"},
// $ {"argument":"Result","direction":"out","type":"integer"}
// $ ]}]}
property Contract: RawUtf8
read fContract;
/// the published service contract, as expected by both client and server
// - by default, will contain ContractHash property value (for security)
// - but you can override this value using plain Contract or any custom
// value (e.g. a custom version number) - in this case, both TServiceFactoryClient
// and TServiceFactoryServer instances must have a matching ContractExpected
// - this value is returned by a '_contract_' pseudo-method name, with the URI:
// $ POST /root/Interface._contract_
// or (if TRestRoutingJsonRpc is used):
// $ POST /root/Interface
// $ (...)
// $ {"method":"_contract_","params":[]}
// (e.g. to be checked in TServiceFactoryClient.Create constructor)
// - if set to SERVICE_CONTRACT_NONE_EXPECTED (i.e. '*'), the client won't
// check and ask the server contract for consistency: it may be used e.g.
// for accessing a plain REST HTTP server which is not based on mORMot,
// so may not implement POST /root/Interface._contract_
property ContractExpected: RawUtf8
read fContractExpected write fContractExpected;
/// direct access to the low-level per-method execution rights
property Execution: TServiceFactoryExecutionDynArray
read fExecution;
published
/// access to the associated TRest ORM instance
property ORM: IRestOrm
read fOrm;
published
/// the registered Interface URI
// - in fact this is the Interface name without the initial 'I', e.g.
// 'Calculator' for ICalculator
property InterfaceUri: RawUtf8
read fInterfaceUri;
/// the registered Interface mangled URI
// - in fact this is encoding the GUID using BinToBase64Uri(), e.g.
// ! ['{c9a646d3-9c61-4cb7-bfcd-ee2522c8f633}'] into '00amyWGct0y_ze4lIsj2Mw'
// - can be substituted to the clear InterfaceUri name
property InterfaceMangledUri: RawUtf8
read fInterfaceMangledUri;
/// how each class instance is to be created
// - only relevant on the server side; on the client side, this class will
// be accessed only to retrieve a remote access instance, i.e. sicSingle
property InstanceCreation: TServiceInstanceImplementation
read fInstanceCreation;
/// a hash of the service contract, serialized as a JSON string
// - this may be used instead of the JSON signature, to enhance security
// (i.e. if you do not want to publish the available methods, but want
// to check for the proper synchronization of both client and server)
// - a possible value may be: "C351335A7406374C"
property ContractHash: RawUtf8
read fContractHash;
end;
const
/// the Server-side instance implementation patterns without any ID
// - so imFree won't be supported
SERVICE_IMPLEMENTATION_NOID = [sicSingle, sicShared];
function ToText(si: TServiceInstanceImplementation): PShortString; overload;
{ ************ TServiceFactoryServerAbstract Abstract Service Provider }
type
/// abstract TServiceFactoryServer parent with a fluent interface for options
// - defining methods to customize the service implementation on Server side
// - as returned by TRestServer.ServiceDefine and ServiceRegister overloaded methods
// - by default, all methods are allowed to execution: you can call AllowAll,
// DenyAll, Allow or Deny in order to specify your exact security policy
// - those methods returns the self instance to provide a fluent interface
// - defined here to avoid circular references between mormot.rest.server.pas
// and mormot.soa.server.pas
TServiceFactoryServerAbstract = class(TServiceFactory)
protected
fByPassAuthentication: boolean;
fResultAsJsonObject: boolean;
fResultAsJsonObjectWithoutResult: boolean;
fResultAsXMLObject: boolean;
fResultAsXMLObjectIfAcceptOnlyXML: boolean;
fResultAsXMLObjectNameSpace: RawUtf8;
fExcludeServiceLogCustomAnswer: boolean;
function GetAuthGroupIDs(const aGroup: array of RawUtf8;
out IDs: TIDDynArray): boolean;
public
/// allow all methods execution for all TAuthGroup
// - all Groups will be affected by this method (on both client and server sides)
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function AllowAll: TServiceFactoryServerAbstract;
/// allow all methods execution for the specified TAuthGroup ID(s)
// - the specified group ID(s) will be used to authorize remote service
// calls from the client side
// - you can retrieve a TAuthGroup ID from its identifier, as such:
// ! UserGroupID := fServer.MainFieldID(TAuthGroup,'User');
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function AllowAllByID(const aGroupID: array of TID): TServiceFactoryServerAbstract;
/// allow all methods execution for the specified TAuthGroup names
// - is just a wrapper around the other AllowAllByID() method, retrieving the
// Group ID from its main field
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function AllowAllByName(const aGroup: array of RawUtf8): TServiceFactoryServerAbstract;
/// deny all methods execution for all TAuthGroup
// - all Groups will be affected by this method (on both client and server sides)
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function DenyAll: TServiceFactoryServerAbstract;
/// deny all methods execution for the specified TAuthGroup ID(s)
// - the specified group ID(s) will be used to authorize remote service
// calls from the client side
// - you can retrieve a TAuthGroup ID from its identifier, as such:
// ! UserGroupID := fServer.MainFieldID(TAuthGroup,'User');
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function DenyAllByID(const aGroupID: array of TID): TServiceFactoryServerAbstract;
/// dent all methods execution for the specified TAuthGroup names
// - is just a wrapper around the other DenyAllByID() method, retrieving the
// Group ID from its main field
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function DenyAllByName(const aGroup: array of RawUtf8): TServiceFactoryServerAbstract;
/// allow specific methods execution for the all TAuthGroup
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - all Groups will be affected by this method (on both client and server sides)
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function Allow(const aMethod: array of RawUtf8): TServiceFactoryServerAbstract;
/// allow specific methods execution for the specified TAuthGroup ID(s)
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - the specified group ID(s) will be used to authorize remote service
// calls from the client side
// - you can retrieve a TAuthGroup ID from its identifier, as such:
// ! UserGroupID := fServer.MainFieldID(TAuthGroup,'User');
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function AllowByID(const aMethod: array of RawUtf8;
const aGroupID: array of TID): TServiceFactoryServerAbstract;
/// allow specific methods execution for the specified TAuthGroup name(s)
// - is just a wrapper around the other AllowByID() method, retrieving the
// Group ID from its main field
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function AllowByName(const aMethod: array of RawUtf8;
const aGroup: array of RawUtf8): TServiceFactoryServerAbstract;
/// deny specific methods execution for the all TAuthGroup
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - all Groups will be affected by this method (on both client and server sides)
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function Deny(const aMethod: array of RawUtf8): TServiceFactoryServerAbstract;
/// deny specific methods execution for the specified TAuthGroup ID(s)
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - the specified group ID(s) will be used to unauthorize remote service
// calls from the client side
// - you can retrieve a TAuthGroup ID from its identifier, as such:
// ! UserGroupID := fServer.MainFieldID(TAuthGroup,'User');
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function DenyByID(const aMethod: array of RawUtf8;
const aGroupID: array of TID): TServiceFactoryServerAbstract; overload;
/// deny specific methods execution for the specified TAuthGroup name(s)
// - is just a wrapper around the other DenyByID() method, retrieving the
// Group ID from its main field
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function DenyByName(const aMethod: array of RawUtf8;
const aGroup: array of RawUtf8): TServiceFactoryServerAbstract;
/// define execution options for a given set of methods
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - if no method name is given (i.e. []), option will be set for all methods
// - include optExecInMainThread will force the method(s) to be called within
// a RunningThread.Synchronize() call - slower, but thread-safe
// - this method returns self in order to allow direct chaining of settings
// calls, in a fluent interface
function SetOptions(const aMethod: array of RawUtf8;
aOptions: TInterfaceMethodOptions;
aAction: TServiceMethodOptionsAction = moaReplace): TServiceFactoryServerAbstract;
/// define execution options for the whole interface
// - fluent alternative of setting homonymous boolean properties of this class
// - this method returns self in order to allow direct chaining of settings
function SetWholeOptions(aOptions: TInterfaceOptions): TServiceFactoryServerAbstract;
/// define the instance life time-out, in seconds
// - for sicClientDriven, sicPerSession, sicPerUser or sicPerGroup modes
// - raise an exception for other kind of execution
// - this method returns self in order to allow direct chaining of setting
// calls for the service, in a fluent interface
function SetTimeoutSec(value: cardinal): TServiceFactoryServerAbstract;
virtual; abstract;
/// log method execution information to a TOrmServiceLog table
// - methods names should be specified as an array (e.g. ['Add','Multiply'])
// - if no method name is given (i.e. []), option will be set for all methods
// - will write to the specified aLogRest instance, and will disable
// writing if aLogRest is nil
// - will write to a (inherited) TOrmServiceLog table, as available in
// TRest's model, unless a dedicated table is specified as aLogClass
// - this method returns self in order to allow direct chaining of security
// calls, in a fluent interface
function SetServiceLog(const aMethod: array of RawUtf8;
const aLogRest: IRestOrm;
aLogClass: TOrmServiceLogClass = nil): TServiceFactoryServerAbstract;
virtual; abstract;
/// set to TRUE disable Authentication method check for the whole interface
// - by default (FALSE), all interface-based services will require valid
// RESTful authentication (if enabled on the server side); setting TRUE will
// disable authentication for all methods of this interface
// (e.g. for returning some HTML content from a public URI, or to implement
// a public service catalog)
property ByPassAuthentication: boolean
read fByPassAuthentication write fByPassAuthentication;
/// set to TRUE to return the interface's methods result as JSON object
// - by default (FALSE), any method execution will return a JSON array with
// all VAR/OUT parameters, in order
// - TRUE will generate a JSON object instead, with the VAR/OUT parameter
// names as field names (and "Result" for any function result) - may be
// useful e.g. when working with JavaScript clients
// - Delphi clients (i.e. TServiceFactoryClient/TInterfacedObjectFake) will
// transparently handle both formats
// - this value can be overridden by setting ForceServiceResultAsJsonObject
// for a given TRestServerUriContext (e.g. for server-side JavaScript work)
property ResultAsJsonObject: boolean
read fResultAsJsonObject write fResultAsJsonObject;
/// set to TRUE to return the interface's methods result as JSON object
// with no '{"result":{...}}' nesting
// - could be used e.g. for plain non mORMot REST Client with in sicSingle
// or sicShared mode kind of services
// - on client side, consider using TRestClientUri.ServiceDefineSharedApi
property ResultAsJsonObjectWithoutResult: boolean
read fResultAsJsonObjectWithoutResult write fResultAsJsonObjectWithoutResult;
/// set to TRUE to return the interface's methods result as XML object
// - by default (FALSE), method execution will return a JSON array with
// all VAR/OUT parameters, or a JSON object if ResultAsJsonObject is TRUE
// - TRUE will generate a XML object instead, with the VAR/OUT parameter
// names as field names (and "Result" for any function result) - may be
// useful e.g. when working with some XML-only clients
// - Delphi clients (i.e. TServiceFactoryClient/TInterfacedObjectFake) does
// NOT handle this XML format yet
// - this value can be overridden by setting ForceServiceResultAsXMLObject
// for a given TRestServerUriContext instance
property ResultAsXMLObject: boolean
read fResultAsXMLObject write fResultAsXMLObject;
/// set to TRUE to return XML objects for the interface's methods result
// if the Accept: HTTP header is exactly 'application/xml' or 'text/xml'
// - the header should be exactly 'Accept: application/xml' or
// 'Accept: text/xml' (and no other value)
// - in this case, ForceServiceResultAsXMLObject will be set for this
// particular TRestServerUriContext instance, and result returned as XML
// - using this method allows to mix standard JSON requests (from JSON
// or AJAX clients) and XML requests (from XML-only clients)
property ResultAsXMLObjectIfAcceptOnlyXML: boolean
read fResultAsXMLObjectIfAcceptOnlyXML write fResultAsXMLObjectIfAcceptOnlyXML;
/// specify a custom name space content when returning a XML object
// - by default, no name space will be appended - but such rough XML will
// have potential validation problems
// - you may use e.g. XMLUTF8_NAMESPACE, which will append <content ...> ...
// </content> around the generated XML data
property ResultAsXMLObjectNameSpace: RawUtf8
read fResultAsXMLObjectNameSpace write fResultAsXMLObjectNameSpace;
/// disable base64-encoded TOrmServiceLog.Output for methods
// returning TServiceCustomAnswer record (to reduce storage size)
property ExcludeServiceLogCustomAnswer: boolean
read fExcludeServiceLogCustomAnswer write fExcludeServiceLogCustomAnswer;
end;
{ ************ TServiceContainer Abstract Services Holder }
type
/// used to lookup one service in a global list of interface-based services
TServiceContainerInterface = record
/// one 'service' item, as set at URI, e.g. 'Calculator'
InterfaceName: RawUtf8;
/// the associated service provider
Service: TServiceFactory;
end;
/// pointer to one lookup in a global list of interface-based services
PServiceContainerInterface = ^TServiceContainerInterface;
/// used to store all s in a global list of interface-based services
TServiceContainerInterfaces = array of TServiceContainerInterface;
/// used to lookup one method in a global list of interface-based services
TServiceContainerInterfaceMethod = record
/// one 'service.method' item, as set at URI
// - e.g.'Calculator.Add','Calculator.Multiply'...
InterfaceDotMethodName: RawUtf8;
/// the associated service provider
InterfaceService: TServiceFactory;
/// the index of the method for the given service
// - 0..3 indicates _free_/_contract_/_signature_/_interface_ pseudo-methods
// - then points to InterfaceService.Interface.
// Methods[InterfaceMethodIndex - SERVICE_PSEUDO_METHOD_COUNT]
InterfaceMethodIndex: integer;
end;
/// pointer to one method lookup in a global list of interface-based services
PServiceContainerInterfaceMethod = ^TServiceContainerInterfaceMethod;
/// used to store all methods in a global list of interface-based services
TServiceContainerInterfaceMethods = array of TServiceContainerInterfaceMethod;
/// used in TServiceContainer to identify fListInterfaceMethod[] entries
// - maximum bit count of 255 is a limitation of the pascal compiler itself
TServiceContainerInterfaceMethodBits = set of 0..255;
/// a global services provider class
// - used to maintain a list of interfaces implementation
// - inherits from TInterfaceResolverInjected and its Resolve() methods,
// compatible with TInjectableObject
TServiceContainer = class(TInterfaceResolverInjected)
protected
fOwner: TInterfaceResolver; // is a TRest instance
// list of services ['Calculator',...]
fInterface: TServiceContainerInterfaces;
fInterfaces: TDynArrayHashed;
// list of service.method ['Calculator.Add','Calculator.Multiply',...]
fInterfaceMethod: TServiceContainerInterfaceMethods;
fInterfaceMethods: TDynArrayHashed;
fExpectMangledUri: boolean;
procedure SetExpectMangledUri(Mangled: boolean);
procedure SetInterfaceMethodBits(MethodNamesCsv: PUtf8Char;
IncludePseudoMethods: boolean; out bits: TServiceContainerInterfaceMethodBits);
function GetMethodName(ListInterfaceMethodIndex: integer): RawUtf8;
procedure CheckInterface(const aInterfaces: array of PRttiInfo);
procedure ClearServiceList; virtual;
function AddServiceInternal(aService: TServiceFactory): PtrInt; virtual;
function AddServiceMethodInternal(const aInterfaceDotMethodName: RawUtf8;
aService: TServiceFactory; var aMethodIndex: integer): PServiceContainerInterfaceMethod; virtual;
function TryResolve(aInterface: PRttiInfo; out Obj): boolean; override;
/// retrieve a service provider from its URI
function GetService(const aUri: RawUtf8): TServiceFactory;
public
/// initialize the Services list
// - supplied TInterfaceResolver should be able to resolve IRestOrm,
// and is typically a TRest instance
constructor Create(aOwner: TInterfaceResolver); virtual;
/// release all registered services
destructor Destroy; override;
/// return the number of registered service interfaces
// - you can use InterfaceList[] to access the instances
function Count: integer;
{$ifdef HASINLINE}inline;{$endif}
/// retrieve a service provider from its index in the list
// - returns nil if out of range index
function Index(aIndex: integer): TServiceFactory; overload;
{$ifdef HASINLINE}inline;{$endif}
/// retrieve a service provider from its GUID / Interface type
// - you shall have registered the interface by a previous call to
// ! TInterfaceFactory.RegisterInterfaces([TypeInfo(IMyInterface),...])
// - on match, it will return the service the corresponding interface factory
// - returns nil if the GUID does not match any registered interface
// - can be used as such to resolve an I: ICalculator interface
// ! if fClient.Services.Info(ICalculator).Get(I) then
// ! ... use I
{$ifdef FPC_HAS_CONSTREF}
function Info(constref aGuid: TGuid): TServiceFactory; overload;
{$else}
function Info(const aGuid: TGuid): TServiceFactory; overload;
{$endif FPC_HAS_CONSTREF}
/// retrieve a service provider from its type information
// - on match, it will return the service the corresponding interface factory
// - returns nil if the type information does not match any registered interface
// - can be used as such to resolve an I: ICalculator interface
// ! if fClient.Services.Info(TypeInfo(ICalculator)).Get(I) then
// ! ... use I
// - is defined as virtual so that e.g. TServiceContainerClient will
// automatically register the interface, if it was not already done
function Info(aTypeInfo: PRttiInfo): TServiceFactory; overload; virtual;
/// notify the other side that the given Callback event interface is released
// - this default implementation will do nothing
function CallBackUnRegister(const Callback: IInvokable): boolean; virtual;
/// retrieve all registered Services GUID
procedure SetGuids(out Services: TGuidDynArray);
/// retrieve all registered Services names
// - i.e. all interface names without the initial 'I', e.g. 'Calculator' for
// ICalculator
procedure SetInterfaceNames(out Names: TRawUtf8DynArray);
/// retrieve all registered Services contracts as a JSON array
// - i.e. a JSON array of TServiceFactory.Contract JSON objects
function AsJson: RawJson;
/// retrieve a service provider from its URI
// - it expects the supplied URI variable to be e.g. '00amyWGct0y_ze4lIsj2Mw'
// or 'Calculator', depending on the ExpectMangledUri property
// - on match, it will return the service the corresponding interface factory
// - returns nil if the URI does not match any registered interface
property Services[const aUri: RawUtf8]: TServiceFactory
read GetService; default;
/// direct access to the internal list of interfdce services ['Calculator',...]
property InterfaceList: TServiceContainerInterfaces
read fInterface;
/// direct access to the internal list of service.method
// - e.g. ['Calculator.Add','Calculator.Multiply',...]
property InterfaceMethod: TServiceContainerInterfaceMethods
read fInterfaceMethod;
/// direct access to the internal list of service.method wrapper
// - e.g. storing 'Calculator.Add', 'Calculator.Multiply', ... values
property InterfaceMethods: TDynArrayHashed
read fInterfaceMethods;
/// the associated TRest instance, owning these services
property Owner: TInterfaceResolver
read fOwner;
/// set if the URI is expected to be mangled from the GUID
// - by default (FALSE), the clear service name is expected to be supplied at
// the URI level (e.g. 'Calculator')
// - if this property is set to TRUE, the mangled URI value will be expected
// instead (may enhance security) - e.g. '00amyWGct0y_ze4lIsj2Mw'
property ExpectMangledUri: boolean
read fExpectMangledUri write SetExpectMangledUri;
end;
{ ************ SOA Related Interfaces }
type
/// prototype of a class implementing redirection of a given interface
// - as returned e.g. by TRest.MultiRedirect method
// - can be used as a main callback, then call Redirect() to manage
// an internal list of redirections
// - when you release this instance, will call Rest.Service.CallbackUnregister
// with the associated fake callback generated
IMultiCallbackRedirect = interface
['{E803A30A-8C06-4BB9-94E6-EB87EACFE980}']
/// add or remove an interface callback to the internal redirection list
// - will register a callback if aSubscribe is true
// - will unregister a callback if aSubscribe is false
// - supplied aCallback shoud implement the expected interface GUID
// - this method will be implemented as thread-safe
// - you can specify some method names, or all methods redirection if []
procedure Redirect(const aCallback: IInvokable;
const aMethodsNames: array of RawUtf8; aSubscribe: boolean = true); overload;
/// add or remove a class instance callback to the internal redirection list
// - will register a callback if aSubscribe is true
// - will unregister a callback if aSubscribe is false
// - supplied aCallback instance should implement the expected interface GUID
// - this method will be implemented as thread-safe
// - you can specify some method names, or all methods redirection if []
procedure Redirect(const aCallback: TInterfacedObject;
const aMethodsNames: array of RawUtf8; aSubscribe: boolean = true); overload;
end;
/// service definition with a method which will be called when a callback
// interface instance is released on the client side
// - may be used to implement safe publish/subscribe mechanism using
// interface callbacks, e.g. over WebSockets
IServiceWithCallbackReleased = interface(IInvokable)
['{8D518FCB-62C3-42EB-9AE7-96ED322140F7}']
/// will be called when a callback is released on the client side
// - this method matches the TInterfaceFactory.MethodIndexCallbackReleased
// signature, so that it will be called with the interface instance by
// TServiceContainerServer.ReleaseFakeCallback
// - you may use it as such - see sample restws_chatserver.dpr:
// ! procedure TChatService.CallbackReleased(const callback: IInvokable;
// ! const interfaceName: RawUtf8);
// ! begin // unsubscribe from fConnected: array of IChatCallback
// ! if interfaceName = 'IChatCallback' then
// ! InterfaceArrayDelete(fConnected, callback);
// ! end;
procedure CallbackReleased(const callback: IInvokable;
const interfaceName: RawUtf8);
end;
/// a callback interface used to notify a TOrm modification in real time
// - will be used e.g. by TRestServer.RecordVersionSynchronizeSubscribeMaster()
// - all methods of this interface will be called asynchronously when
// transmitted via our WebSockets implementation, since they are defined as
// plain procedures
// - each callback instance should be private to a specific TOrm
IServiceRecordVersionCallback = interface(IInvokable)
['{8598E6BE-3590-4F76-9449-7AF7AF4241B0}']
/// this event will be raised on any Add on a versioned record
// - the supplied JSON object will contain the TRecordVersion field
procedure Added(const NewContent: RawJson);
/// this event will be raised on any Update on a versioned record
// - the supplied JSON object will contain the TRecordVersion field
procedure Updated(const ModifiedContent: RawJson);
/// this event will be raised on any Delete on a versioned record
procedure Deleted(const ID: TID; const Revision: TRecordVersion);
/// allow to optimize process for WebSockets "jumbo frame" items
// - this method may be called with isLast=false before the first method
// call of this interface, then with isLast=true after the call of the
// last method of the "jumbo frame"
// - match TInterfaceFactory.MethodIndexCurrentFrameCallback signature
// - allow e.g. to create a temporary TRestBatch for jumbo frames
// - if individual frames are received, this method won't be called
procedure CurrentFrame(isLast: boolean);
end;
/// a list of callback interfaces to notify TOrm modifications
// - you can use InterfaceArray*() wrapper functions to manage the list
IServiceRecordVersionCallbackDynArray = array of IServiceRecordVersionCallback;
{ ***************** TServicesPublishedInterfacesList Services Catalog }
type
/// a specialized UTF-8 string type, used for TRestServerUri storage
// - URI format is 'address:port/root', but port or root are optional
// - you could use TRestServerUri record to store and process it
TRestServerUriString = type RawUtf8;
/// a list of UTF-8 strings, used for TRestServerUri storage
// - URI format is 'address:port/root', but port or root are optional
// - you could use TRestServerUri record to store and process each item
TRestServerUriStringDynArray = array of TRestServerUriString;
/// used to access a TRestServer from its TRestServerUriString URI
// - URI format is 'address:port/root', and may be transmitted as
// TRestServerUriString text instances
{$ifdef USERECORDWITHMETHODS}
TRestServerUri = record
{$else}
TRestServerUri = object
{$endif USERECORDWITHMETHODS}
private
function GetUri: TRestServerUriString;
procedure SetUri(const Value: TRestServerUriString);
public
/// the TRestServer IP Address or DNS name
Address: RawUtf8;
/// the TRestServer IP port
Port: RawUtf8;
/// the TRestServer model Root
Root: RawUtf8;
/// returns TRUE if all field values do match, case insensitively
function Equals(const other: TRestServerUri): boolean;
/// property which allows to read or set the Address/Port/Root fields as
// one UTF-8 text field (i.e. a TRestServerUriString instance)
// - URI format is 'address:port/root', but port or root are optional
property URI: TRestServerUriString
read GetUri write SetURI;
end;
/// store a list of TRestServer URIs
TRestServerUriDynArray = array of TRestServerUri;
/// used to publish all Services supported by a TRestServer instance
// - as expected by TRestServer.ServicesPublishedInterfaces
// - can be serialized as a JSON object via RecordLoadJson/RecordSaveJson
{$ifdef USERECORDWITHMETHODS}
TServicesPublishedInterfaces = record
{$else}
TServicesPublishedInterfaces = object
{$endif USERECORDWITHMETHODS}
public
/// how this TRestServer could be accessed
PublicUri: TRestServerUri;
/// the list of supported services names
// - in fact this is the Interface name without the initial 'I', e.g.
// 'Calculator' for ICalculator
Names: TRawUtf8DynArray;
end;
/// store a list of published Services supported by a TRestServer instance
TServicesPublishedInterfacesDynArray = array of TServicesPublishedInterfaces;
/// used e.g. by TRestServer to store a list of TServicesPublishedInterfaces
TServicesPublishedInterfacesList = class(TObjectRWLightLock)
private
fDynArray: TDynArray;
fDynArrayTimeoutTix: TDynArray;
fTimeoutTix: TInt64DynArray;
fTimeoutTixCount: integer;
fLastPublishedJson: cardinal;
fTimeOut: integer;
public
/// the internal list of published services
// - the list is stored in-order, i.e. it will follow the RegisterFromJson()
// execution order: the latest registrations will appear last
List: TServicesPublishedInterfacesDynArray;
/// how many items are actually stored in List[]
Count: integer;
/// initialize the storage
// - an optional time out period, in milliseconds, may be defined - but the
// clients should ensure that RegisterFromClientJson() is called in order
// to refresh the list (e.g. from _contract_ HTTP body)
constructor Create(aTimeoutMS: integer); reintroduce; virtual;
/// add the JSON serialized TServicesPublishedInterfaces to the list
// - called by TRestServerUriContext.InternalExecuteSoaByInterface when
// the client provides its own services as _contract_ HTTP body
// - warning: supplied PublishedJson will be parsed in place, so modified
procedure RegisterFromClientJson(var PublishedJson: RawUtf8);
/// set the list from JSON serialized TServicesPublishedInterfacesDynArray
// - may be used to duplicate the whole TRestServer.AssociatedServices
// content, as returned from /root/Stat?findservice=*
// - warning: supplied PublishedJson will be parsed in place, so modified
procedure RegisterFromServerJson(var PublishedJson: RawUtf8);
/// search for a public URI in the registration list
function FindUri(const aPublicUri: TRestServerUri): PtrInt;
/// search for the latest registrations of a service, by name
// - will lookup for the Interface name without the initial 'I', e.g.
// 'Calculator' for ICalculator - warning: research is case-sensitive
// - if the service name has been registered several times, all
// registration will be returned, the latest in first position
function FindService(const aServiceName: RawUtf8): TRestServerUriDynArray;
/// return all services URI by name, from the registration list, as URIs
// - will lookup for the Interface name without the initial 'I', e.g.
// 'Calculator' for ICalculator - warning: research is case-sensitive
// - the returned string will contain all matching server URI, the latest
// registration being the first to appear, e.g.
// $ ["addresslast:port/root","addressprevious:port/root","addressfirst:port/root"]
function FindServiceAll(
const aServiceName: RawUtf8): TRestServerUriStringDynArray; overload;
/// return all services URI by name, from the registration list, as JSON
// - will lookup for the Interface name without the initial 'I', e.g.
// 'Calculator' for ICalculator - warning: research is case-sensitive
// - the returned JSON array will contain all matching server URI, encoded as
// a TRestServerUri JSON array, the latest registration being
// the first to appear, e.g.
// $ [{"Address":"addresslast","Port":"port","Root":"root"},...]
// - if aServiceName='*', it will return ALL registration items, encoded as
// a TServicesPublishedInterfaces JSON array, e.g.
// $ [{"PublicUri":{"Address":"1.2.3.4","Port":"123","Root":"root"},"Names":['Calculator']},...]
procedure FindServiceAll(const aServiceName: RawUtf8;
aWriter: TJsonWriter); overload;
/// the number of milliseconds after which an entry expires
// - is 0 by default, meaning no expiration
// - you can set it to a value so that any service URI registered with
// RegisterFromJson() AFTER this property modification may expire
property TimeOut: integer
read fTimeOut write fTimeOut;
end;
implementation
{ ************ TOrmServiceLog TOrmServiceNotifications Classes }
{ TOrmServiceLog }
class procedure TOrmServiceLog.InitializeTable(const Server: IRestOrmServer;
const FieldName: RawUtf8; Options: TOrmInitializeTableOptions);
begin
inherited;
if FieldName = '' then
Server.CreateSqlMultiIndex(self, ['Method', 'MicroSec'], false);
end;
{ TOrmServiceNotifications }
class procedure TOrmServiceNotifications.InitializeTable(
const Server: IRestOrmServer; const FieldName: RawUtf8;
Options: TOrmInitializeTableOptions);
begin
inherited;
if (FieldName = '') or
(FieldName = 'Sent') then