Jump to content
Bjørn Larsen

Custom media type

Recommended Posts

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

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

  • Thanks 1

Share this post


Link to post

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

 

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×