Bjørn Larsen 3 Posted December 11, 2018 Hi, I need to implement RFC 7643 (System for Cross-domain Identity Management) where the content-type must be "application/scim+json". Is it possible and how do I add this as a new media type? I have tried [GET, Path('users/{UserID}'), Produces('application/scim+json')] but this results in loosing all quoting of names and values in the JSON result. I am able to archive the desired result by using TMediaType.APPLICATION_JSON and manipulate the TWebResponse directly, but then I need to also set content-encoding. Doesn't seems like a smooth solution. Is this the best way to solve this? Best regards, Bjørn Share this post Link to post
Andrea Magni 75 Posted December 13, 2018 Hi @Bjørn Larsen, You have a number of possibilities and I would choose one way or another depending on the actual use case. If you need a single (or just a few) methods to produce application/scim+json content type, I would probably follow the way to let MARS produce standard JSON and then fix the ContentType/ContentEncoding. There's a handy way to do this through the Context attribute (MARS will inject a reference to the Response object you will be able to use it across all the methods you need): [Path('helloworld')] THelloWorldResource = class protected [Context] Response: TWebResponse; // add unit Web.HTTPApp to uses clause public [GET, Produces(TMediaType.APPLICATION_JSON)] function User1: TJSONObject; end; (...) function THelloWorldResource.User1: TJSONObject; begin Result := TJSONObject.Create; Result.WriteStringValue('id', TGUID.NewGuid.ToString); Response.ContentType := 'application/scim+json'; Response.ContentEncoding := 'UTF-8'; end; If you have several methods (in a single resource) that need to produce application/scim+json and you want to generalize this "response patching" one for all, you may take advantage of another MARS functionality: AfterInvoke code execution. It comes in a couple of variations, the first one will let you add a method (PatchResponse) and decorate it in order to have MARS execute it after each request to that resource: [Path('helloworld')] THelloWorldResource = class protected [Context] Response: TWebResponse; public [GET, Produces(TMediaType.APPLICATION_JSON)] function User1: TJSONObject; [AfterInvoke] procedure PatchResponse; end; (...) procedure THelloWorldResource.PatchResponse; begin Response.ContentType := 'application/scim+json'; Response.ContentEncoding := 'UTF-8'; end; function THelloWorldResource.User1: TJSONObject; begin Result := TJSONObject.Create; Result.WriteStringValue('id', TGUID.NewGuid.ToString); end; This way, your User1 (and other) method will not be polluted by the "response patching" and at the same time you have a single copy of the "response patching" code (DRY principle). Another AfterInvoke possibility is to define it at a more general level by calling TMARSActivation.RegisterAfterInvoke: TMARSActivation.RegisterAfterInvoke( procedure (const AActivation: IMARSActivation) begin if AActivation.ResourceInstance is THelloWorldResource then // or whatever other strategy begin AActivation.Response.ContentType := 'application/scim+json'; AActivation.Response.ContentEncoding := 'UTF-8'; end; end ); This has the advantage to be very wide (possibly spanning over each MARS engine/application) and does not require the injection of the Response object at resource level. You can setup your own strategy to determine when it's the case to patch the response or not (i.e. use some custom attribute yourself to decorate resources/methods or check the current ContentType or whatever... the AActivation parameter will provide you a very detailed context over the current MARS activation including Request, Response, selected class, selected method, method return value and so on). These are all "patching" options. There is another option available though: define a new media type. From the example you posted, I can't see if you are returning standard or custom types (i.e. TJSONObject instances or TYourClass instances/records). In both cases you can define a new MBW (MessageBodyWriter) for a new media type (application/scim+json) and have full control over Response generation. It's easy, I am attaching here a unit (MBW.ScimPlusJSON.pas) with a new MBW implementation for application/scim+json that will match any request with a Produces('application/scim+json') attribute and will act the same of the standard JSON MBW with the addition of setting the ContentEncoding of the Response. (ContenType will automatically be set by MARS). Just be sure to include the unit in the uses list of your resources unit or in the Server.Ignition unit. You resource then will look like this: [Path('helloworld')] THelloWorldResource = class protected [Context] Response: TWebResponse; public [GET, Produces(MEDIATYPE_APPLICATION_SCIM_PLUS_JSON)] function User1: TJSONObject; end; (...) function THelloWorldResource.User1: TJSONObject; begin Result := TJSONObject.Create; Result.WriteStringValue('id', TGUID.NewGuid.ToString); end; Hope this helps you to solve your problem and please let me know if something is unclear or you need anything else. Sincerely, Andrea MBW.ScimPlusJSON.pas 1 Share this post Link to post
Bjørn Larsen 3 Posted December 14, 2018 Hi @Andrea Magni, Thanks for another great solution. I went with your solution for defining a new media type. This way I can mix the returning media types without a lot of clutter. Most of the methods returns records, so I just "faked" a new writer this way 🙂 [Produces(MEDIATYPE_APPLICATION_SCIM_PLUS_JSON)] TScimRecordWriter = class(TRecordWriter); At first I could not get it to work, but finally I figured out that I had to registered the writers in the correct order. Otherwise the first writer would always be triggered. Best regards, Bjørn Share this post Link to post
Andrea Magni 75 Posted December 18, 2018 On 12/14/2018 at 2:40 PM, Bjørn Larsen said: At first I could not get it to work, but finally I figured out that I had to registered the writers in the correct order. Otherwise the first writer would always be triggered. Apart from the registration order, you can define a higher affinity of your writer in order to supersede the standard ones. Just switch the value returned by the affinity function in the RegisterWriter call from TMARSMessageBodyRegistry.AFFINITY_MEDIUM to TMARSMessageBodyRegistry.AFFINITY_HIGH and your writer will have a priority over the standard record writer. I would then write the WriteTo implementation like this: procedure TScimPlusJSONWriter.WriteTo(const AValue: TValue; const AMediaType: TMediaType; AOutputStream: TStream; const AActivation: IMARSActivation); var LType: TRttiType; begin LType := AActivation.MethodReturnType; if not Assigned(LType) then Exit; // procedure? if LType.IsRecord or LType.IsDynamicArrayOfRecord then TMARSMessageBodyWriter.WriteWith<TRecordWriter>(AValue, AMediaType, AOutputStream, AActivation) else if LType.IsObjectOfType<TJSONValue> then TMARSMessageBodyWriter.WriteWith<TJSONValueWriter>(AValue, AMediaType, AOutputStream, AActivation); AActivation.Response.ContentEncoding := 'UTF-8'; end; TMARSMessageBodyWriter.WriteWith<> is a new utility function I just introduced in the "develop" branch (MARS.Core.MessageBodyWriter unit, https://github.com/andrea-magni/MARS/commit/e818d7e261e7ef2b9a1c2c714adae459c5585c56#diff-97e07c61cdc46902bf53d808b8117104R157 ). You can simply instantiate the corresponding MessageBodyWriter class as we did before. Sincerely, 1 Share this post Link to post