Can RemObjects SDK parameters be passed via the URI?
Asked Answered
V

3

5

We have a RemObjects SDK HTTP server that exposes a number of services and methods. Is it possible to call a method via a URI rather than passing the parameters as SOAP/JSON e.g.

http://www.mywebservice.com/servicename/methodname?param1=xxx&param2=yyy
Vevay answered 1/2, 2013 at 11:10 Comment(6)
You'd probably get a more direct answer from the vendor, at connect.remobjects.com, not that this isn't an okay place for this question.Airt
@WarrenP-Normally, I would agree - RemObjects have great products, but their support can take a long time to respond. I though it might be quicker posting here :)Vevay
@Vevay - if they ever respond...Insolate
@WarrenP, they're also happy to tell you "We do not support that feature, nor intend to implement it" - while on SO, even if something is not supported by RO, you might still find a way to do it.Bagehot
@norgepaul, what would the result look like? The stuff that the server returns?Bagehot
@CosminPrund - The result is just some JSON formatted text.Vevay
V
3

UPDATE

I've written an improved version of the server descendant. This one converts a formatted URI into a JSON object that will subsequently be handled by the RO JSON Message handler.

The default handing method is to ignore the URI.

Change URIHandlingMethod to urhJSON to accept a URI like this:

http://www.mywebservice.com/json?{JSON OBJECT}

Set URIHandlingMethod to urhParametersto to accept a URI like this:

http://www.mywebservice.com/json/service/method?param1=xxx&param2=yyy

Here's the code:

unit ROJSONURIIndyHTTPServer ;

interface

uses
  SysUtils, Classes,

  uROIndyHTTPServer,

  IdURI, IdCustomHTTPServer;

type
  TURIHandlingMethod = (
    urhNone,
    urhJSON,
    urhParameters
  );

  TROJSONURIIndyHTTPServer = class(TROIndyHTTPServer)
  private
    FURIHandlingMethod: TURIHandlingMethod;
    FJSONVersion: String;

    function ConvertURIToJSON(const Document, Params: String): String;
    function NextBlock(var Value: String; Delimiter: Char = '/'): String;
  protected
    procedure InternalServerCommandGet(AThread: TIdThreadClass; RequestInfo: TIdHTTPRequestInfo; ResponseInfo: TIdHTTPResponseInfo); override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property URIHandlingMethod: TURIHandlingMethod read FURIHandlingMethod write FURIHandlingMethod;
    property JSONVersion: String read FJSONVersion write FJSONVersion;
  end;

implementation

{ TROJSONURIIndyHTTPServer }

constructor TROJSONURIIndyHTTPServer.Create(AOwner: TComponent);
begin
  inherited;

  FJSONVersion := '1.1';
end;

function TROJSONURIIndyHTTPServer.NextBlock(var Value: String; Delimiter: Char): String;
var
  p: Integer;
begin
  p := 1;

  while (p <= length(Value)) and (Value[p] <> Delimiter) do
    Inc(p);

  if p = length(Value) then
    Result := Value
  else
    Result := copy(Value, 1, p - 1);

  Value := copy(Value, p + 1, MaxInt);
end;

function TROJSONURIIndyHTTPServer.ConvertURIToJSON(const Document, Params: String): String;
const
  JSONObjectTemplate = '{"method":"%s.%s"%s,"version": "%s"}';
  JSONParamTemplate = '"%s":"%s"';
  JSONParamsTemplate = ',"params":{%s}';
var
  CallService, CallMessage,
  ParsedDocument, ParsedParams, JSONParams,
  Param, ParamName, ParamValue: String;
  i: Integer;
begin
  Result := '';

  ParsedDocument := Trim(Document);

  // Remove the leading /
  if (length(Document) > 0) and
     (Document[1] = '/') then
    NextBlock(ParsedDocument);

  // Remove the message type
  NextBlock(ParsedDocument);

  // Extract the service
  CallService := NextBlock(ParsedDocument);

  // Exctract the service message (method)
  CallMessage := NextBlock(ParsedDocument);

  JSONParams := '';
  ParsedParams := Params;

  while ParsedParams <> '' do
  begin
    // Extract the parameter and value
    Param := NextBlock(ParsedParams, '&');

    // See RFC 1866 section 8.2.1. TP
    Param := StringReplace(Param, '+', ' ', [rfReplaceAll]);  {do not localize}

    // Extract the parameter name
    ParamName := NextBlock(Param, '=');

    // Extract the parameter value
    ParamValue := Param;

    // Add a delimiter if required
    if JSONParams <> '' then
      JSONParams := JSONParams + ',';

    // Build the JSON style parameter
    JSONParams := JSONParams + format(JSONParamTemplate, [ParamName, ParamValue]);
  end;

  if JSONParams <> '' then
    JSONParams := format(JSONParamsTemplate, [JSONParams]);

  // Make sure we have values for all the object variables, then build the JSON object
  if (CallService <> '') and
     (CallMessage <> '') and
     (FJSONVersion <> '') then
    Result := format(JSONObjectTemplate, [CallService, CallMessage, JSONParams, JSONVersion]);
end;

procedure TROJSONURIIndyHTTPServer.InternalServerCommandGet(
  AThread: TIdThreadClass; RequestInfo: TIdHTTPRequestInfo;
  ResponseInfo: TIdHTTPResponseInfo);
begin
  if FURIHandlingMethod in [urhJSON, urhParameters] then
  begin
    // Parse parameters into JSON if required
    if FURIHandlingMethod = urhParameters then
      RequestInfo.UnparsedParams := ConvertURIToJSON(RequestInfo.Document, RequestInfo.UnparsedParams);

    // Decode the URI e.g. converts %20 to whitespace
    RequestInfo.UnparsedParams := TIdURI.URLDecode(RequestInfo.UnparsedParams);

    //  This works around a bug in TROIndyHTTPServer. By adding a whitespace to the
    //  end of the QueryParams it forces the http server to process the parameters
    RequestInfo.QueryParams := TIdURI.URLDecode(RequestInfo.QueryParams) + ' ';
  end;

  inherited;
end;

end.

Original Answer

This is a follow up to André's answer.

With the current version of RemObjects SDK the following URI should work, but doesn't:

http://www.mywebservice.com/JSON?{"id":"{392543cf-f110-4ba3-95471b02ce5bd693}","method":"servicename.methodname","params":{"param1":"xxx","param2":"yyy"}}:

There are 2 reasons why:

  • The URI is not decoded before it is passed to the message handler. This results in a JSON error if any of the characters have been encoded e.g. %20 etc.
  • There seems to be a bug in the ROIndyHTTPServer code that mishandles the URI parameters.

I've created a ROIndyHTTPServer descendant that fixes both problems. Here's the code:

unit FixedROIndyHTTPServer;

interface

uses
  SysUtils, Classes,

  uROIndyHTTPServer,

  IdURI, IdCustomHTTPServer;

type
  TFixedROIndyHTTPServer = class(TROIndyHTTPServer)
  protected
    procedure InternalServerCommandGet(AThread: TIdThreadClass; RequestInfo: TIdHTTPRequestInfo; ResponseInfo: TIdHTTPResponseInfo); override;
  public
    constructor Create(AOwner: TComponent); override;
  end;

implementation

{ TFixedROIndyHTTPServer }

constructor TFixedROIndyHTTPServer.Create(AOwner: TComponent);
begin
  inherited;
end;

procedure TFixedROIndyHTTPServer.InternalServerCommandGet(
  AThread: TIdThreadClass; RequestInfo: TIdHTTPRequestInfo;
  ResponseInfo: TIdHTTPResponseInfo);
begin
  // This fixes 2 issues with TROIndyHTTPServer
  //  1) It decodes the parameters e.g. converts %20 to whitespace
  //  2) It adds a whitespace to the end of the QueryParams. This
  //     forces the http server to process the parameters.

  RequestInfo.QueryParams := TIdURI.URLDecode(RequestInfo.QueryParams) + ' ';
  RequestInfo.UnparsedParams := TIdURI.URLDecode(RequestInfo.UnparsedParams);

  inherited;
end;

end.

This doesn't answer my question, but it is a workaround for anybody having similar problems.

I'm still keen to hear if RO SDK supports the use of custom URIs.

Vevay answered 1/2, 2013 at 14:20 Comment(3)
I think you should still post it on connect.remobjects.comAirt
+1 for urhParametersto; I think this answer should get the accept tick.Bagehot
+CosminPrund - OK, will do. It was your post that gave me the idea :)Vevay
B
3

Here's a play on norgepaul's solution that looks good and returns JSON. It's based on the same idea of intercepting the HTTP request using a descendant of TROIndyHTTPServer, but this time I'm not only fixing the parameters of the request, I'm creating the "JSON" post that the client didn't send!

Here's the code that I used to test with the default "VCL Standalon" server implementation:

TUriROIndyHTTPServer = class(TROIndyHTTPServer)
protected
  procedure InternalServerCommandGet(AThread: TIdThreadClass; RequestInfo: TIdHTTPRequestInfo; ResponseInfo: TIdHTTPResponseInfo); override;
end;

procedure TUriROIndyHTTPServer.InternalServerCommandGet(AThread: TIdThreadClass;RequestInfo: TIdHTTPRequestInfo; ResponseInfo: TIdHTTPResponseInfo);
var A, B: Integer;
    NewPost: AnsiString;
begin
  if RequestInfo.Document = '/json/sum' then
    begin
      // Extract the parameters
      A := StrToIntDef(RequestInfo.Params.Values['a'], 0);
      B := StrToIntDef(RequestInfo.Params.Values['b'], 0);
      NewPost := AnsiString(Format('{"version":"1.1","method":"NewService.Sum","params":{"A":"%d","B":"%d"}}', [A, B]));

      // Prepare the (fake) post-stream
      RequestInfo.PostStream.Free;
      RequestInfo.PostStream := TMemoryStream.Create;
      RequestInfo.PostStream.Write(NewPost[1], Length(NewPost));
      RequestInfo.PostStream.Position := 0;
    end
  else if RequestInfo.Document = '/json/getservertime' then
    begin
      // Extract the parameters
      NewPost := '{"version":"1.1","method":"NewService.GetServerTime"}';

      // Prepare the (fake) post-stream
      RequestInfo.PostStream.Free;
      RequestInfo.PostStream := TMemoryStream.Create;
      RequestInfo.PostStream.Write(NewPost[1], Length(NewPost));
      RequestInfo.PostStream.Position := 0;
    end;

  inherited;
end;

With this sort of code in place, I can make requests like this:

http://localhost:8080/json/sum?a=1&b=2

returns (in browser!)

 {"version":"1.1","result":"3"}       

and this:

 http://localhost:8080/json/getservertime

returns this (well, at the time of this writing):

{"version":"1.1","result":"2013-02-01T19:24:24.827"}

The result (in browser or foreign application) is pure JSON because it's been formated as a "JSON Message" using RO's code.

Bagehot answered 1/2, 2013 at 17:25 Comment(6)
@cosmin-Nice idea. It would be nice if it was generic and worked with any service/parameter list. It should be pretty easy to implement. I'll have a go tomorrow and post it here if it works.Vevay
@norgepaul, I'm looking into making it generic right now. I'm very new RO-SDK and I'm trying to figure out how services are eventually dispatched. The information is surely in there somewhere. I know for sure we could use the RODL reader to extract the same information straight from the RODL. A solution is definitively possible.Bagehot
@Cosmin-To keep it simple I'd just follow the method you already started here. If you have a URI something like webservice/json/service/method?param1=xxx&param2=yyy just parse out the service, method and parameters then create a JSON object e.g. {"version":"1.1","method":"service.method","params":{"param1":"xxx","param2":"yyy"}} ... That's it! There will certainly be more elegant ways to do it though if you have the time to investigate :)Vevay
This is the kind of hack that's cool, but should NOT be deployed into the field. Who knows what it does to your overall system stability.Airt
@WarrenP, please elaborate. What do you base your statement on? This code doesn't do anything hard to analyze, the word "hack" is just scare-word here. The whole code for TROBaseHTTPServer is implemented in 380 lines of code; All I'm doing is "fake" a post stream; The POST stream is owned by TIdHTTPRequestInfo so I don't need to look to far to realize I'm not going to affect anything else.Bagehot
I guess if it was my company and my dollars, I'd be putting this in its own HTTP server, and not hacking it into the RemObjects SDK's codebase at all. Anyone else is free to do as they wish, of course.Airt
A
2

As far as I know: no.

You only can do some kind of REST call using a JSON struct: http://www.mywebservice.com/JSON?{"id":"{392543cf-f110-4ba3-95471b02ce5bd693}","method":"servicename.methodname","params":{"param1":"xxx","param2":"yyy"}}:

btw: DataAbstract (based on RO) has a REST interface but RO itself not... :(

Asa answered 1/2, 2013 at 11:51 Comment(2)
That's the way I'm making the calls now, but our consumer wants the parameters in the URI. We have DataAbstract, but I doubt that will help as the DA Rest implementation probably only deals with DataAbstract requests, not normal SDK one. Am I correct?Vevay
Sorry, I misread your answer. Currently we post the JSON struct, we don't add it to the URI. However, I tried it and it didn't work. See my answer for a fix that does make it work.Vevay

© 2022 - 2024 — McMap. All rights reserved.