{
    This file is part of the Free Pascal run time library.
    Copyright (c) 2019 by the Free Pascal development team

    SQLDB REST bridge : ADO-styled XML input/output

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
unit sqldbrestado;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, DateUtils, db,fpjson, dom, XMLRead, XMLWrite,sqldbrestschema,sqldbrestio, sqldbrestbridge;

Type

  { TADOInputStreamer }

  TADOInputStreamer = Class(TRestInputStreamer)
  private
    FDataName: UTF8String;
    FRowName: UTF8String;
    FXML: TXMLDocument;
    FPacket : TDOMElement;
    FData : TDOMElement; // Equals FPacket
    FRow : TDOMElement;
  Protected
    function GetNodeText(N: TDOmNode): UnicodeString;
  Public
    Destructor Destroy; override;
    Class Function GetContentType: String; override;
    Function SelectObject(aIndex : Integer) : Boolean; override;
    function GetContentField(aName: UTF8string): TJSONData; override;
    procedure InitStreaming; override;
    Property XML : TXMLDocument Read FXML;
    Property Packet : TDOMElement Read FPacket;
    Property Data : TDOMElement Read FData;
    Property Row : TDOMElement Read FRow;
    Property DataName : UTF8String Read FDataName Write FDataName;
    Property RowName : UTF8String Read FRowName Write FRowName;
  end;

  { TADOOutputStreamer }

  TADOOutputStreamer = Class(TRestOutputStreamer)
  Private
    FDataName: UTF8String;
    FRowName: UTF8String;
    FXML: TXMLDocument;
    FData : TDOMElement; // Equals FRoot
    FRow: TDOMElement;
    FRoot: TDomElement;
    function CreateXSD: TDomElement;
  Public
    procedure EndData; override;
    procedure EndRow; override;
    procedure FinalizeOutput; override;
    procedure StartData; override;
    procedure StartRow; override;
    // Return Nil for null field.
    function FieldToXML(aPair: TRestFieldPair): TDOMElement; virtual;
    procedure WriteField(aPair: TRestFieldPair); override;
    procedure WriteMetadata(aFieldList: TRestFieldPairArray); override;
    Procedure CreateErrorContent(aCode : Integer; Const aMessage: String); override;
    Property XML : TXMLDocument Read FXML;
    Property Data : TDOMelement Read FData;
    Property Row : TDOMelement Read FRow;
  Public
    Destructor Destroy; override;
    Class Function GetContentType: String; override;
    function RequireMetadata : Boolean; override;
    procedure InitStreaming; override;
    Property DataName : UTF8String Read FDataName Write FDataName;
    Property RowName : UTF8String Read FRowName Write FRowName;
  end;

implementation

uses sqldbrestconst;

{ TADOInputStreamer }

destructor TADOInputStreamer.Destroy;
begin
  FreeAndNil(FXML);
  inherited Destroy;
end;

class function TADOInputStreamer.GetContentType: String;
begin
  Result:='text/xml';
end;

function TADOInputStreamer.SelectObject(aIndex: Integer): Boolean;

Var
  N : TDomNode;
  NN : UnicodeString;
begin
  Result:=False;
  NN:=UTF8Decode(RowName);
  N:=FData.FindNode(NN);
  While (aIndex>0) and (N<>Nil) and (N.NodeName<>NN) and (N.NodeType<>ELEMENT_NODE) do
    begin
    N:=N.NextSibling;
    Dec(aIndex);
    end;
  Result:=(aIndex=0) and (N<>Nil);
  If Result then
    FRow:=N as TDomElement
  else
    FRow:=Nil;
end;

function TADOInputStreamer.GetNodeText(N: TDOmNode): UnicodeString;

Var
  V : TDomNode;

begin
  Result:='';
  V:=N.FirstChild;
  While (V<>Nil) and (V.NodeType<>TEXT_NODE) do
    V:=V.NextSibling;
  If Assigned(V) then
    Result:=V.NodeValue;
end;

function TADOInputStreamer.GetContentField(aName: UTF8string): TJSONData;

Var
  NN : UnicodeString;
  N : TDomNode;
begin
  NN:=UTF8Decode(aName);
  N:=FRow.FindNode(NN);
  if Assigned(N) and (N.NodeType=ELEMENT_NODE) then
    Result:=TJSONString.Create(UTF8Encode(GetNodeText(N)));
end;

procedure TADOInputStreamer.InitStreaming;

Var
  Msg : String;
  NN : UnicodeString;

begin
  if DataName='' then
    DataName:='Data';
  if RowName='' then
    RowName:='Row';
  FreeAndNil(FXML);
  if Stream.Size<=0 then
    exit;
  try
    ReadXMLFile(FXML,Stream);
  except
    On E : Exception do
      begin
      Msg:=E.Message;
      FXML:=Nil;
      end;
  end;
  if (FXML=Nil)  then
    Raise ESQLDBRest.CreateFmt(Statuses.GetStatusCode(rsInvalidContent),SErrInvalidXMLInput,[Msg]);
  FPacket:=FXML.DocumentElement;
  NN:=UTF8Decode(DataName);
  if FPacket.NodeName<>NN then
    Raise ESQLDBRest.CreateFmt(Statuses.GetStatusCode(rsInvalidContent),SErrInvalidXMLInput,[SErrMissingDocumentRoot]);
  FData:=FPacket;
end;

{ TADOOutputStreamer }


procedure TADOOutputStreamer.EndData;
begin
  FData:=Nil;
end;

procedure TADOOutputStreamer.EndRow;
begin
  FRow:=Nil;
end;

procedure TADOOutputStreamer.FinalizeOutput;

begin
{$IFNDEF VER3_0}
  if Not (ooHumanReadable in OutputOptions) then
    begin
    With TDOMWriter.Create(Stream,FXML) do
      try
        LineBreak:='';
        IndentSize:=0;
        WriteNode(FXML);
      finally
        Free;
      end;
    end
  else
{$ENDIF}
    xmlwrite.WriteXML(FXML,Stream);
  FreeAndNil(FXML);
end;

procedure TADOOutputStreamer.StartData;
begin
  // Rows are straight under the Data packet
  FData:=FRoot;
end;

procedure TADOOutputStreamer.StartRow;
begin
  if (FRow<>Nil) then
    Raise ESQLDBRest.Create(Statuses.GetStatusCode(rsError),SErrDoubleRowStart);
  FRow:=FXML.CreateElement(UTF8Decode(RowName));
  FData.AppendChild(FRow);
end;

function TADOOutputStreamer.FieldToXML(aPair: TRestFieldPair): TDOMElement;

Var
  F : TField;
  S : UTF8String;

begin
  Result:=Nil;
  F:=aPair.DBField;;
  If (aPair.RestField.FieldType=rftUnknown) then
    raise ESQLDBRest.CreateFmt(Statuses.GetStatusCode(rsError),SErrUnsupportedRestFieldType, [aPair.RestField.PublicName]);
  If (F.IsNull) then
    Exit;
  S:=FieldToString(aPair.RestField.FieldType,F);
  Result:=FXML.CreateElement(UTF8Decode(aPair.RestField.PublicName));
  Result.AppendChild(FXML.CreateTextNode(UTF8Decode(S)));
end;

procedure TADOOutputStreamer.WriteField(aPair: TRestFieldPair);

Var
  D : TDOMElement;
  N : UTF8String;

begin
  N:=aPair.RestField.PublicName;
  if FRow=Nil then
    Raise ESQLDBRest.CreateFmt(Statuses.GetStatusCode(rsError),SErrFieldWithoutRow,[N]);
  D:=FieldToXML(aPair);
  if (D=Nil) and (not HasOption(ooSparse)) then
    D:=FXML.CreateElement(UTF8Decode(aPair.RestField.PublicName));
  if D<>Nil then
    FRow.AppendChild(D);
end;

function TADOOutputStreamer.CreateXSD: TDomElement;

// Create XSD and append to root. Return element to which field list must be appended.

Var
  SN,N,E,TLN : TDomElement;

begin
  SN:=FXML.CreateElement('xs:schema');
  SN['id']:=Utf8Decode(DataName);
  SN['xmlns']:='';
  SN['xmlns:xs']:='http://www.w3.org/2001/XMLSchema';
  SN['xmlns:msdata']:= 'urn:schemas-microsoft-com:xml-msdata';
  FRoot.AppendChild(SN);
  // Add table list with 1 table.
  // Element
  N:=FXML.CreateElement('xs:element');
  SN.AppendChild(N);
  N['name']:=UTF8Decode(DataName);
  N['msdata:IsDataSet']:='true';
  N['msdata:UseCurrentLocale']:='true';
  // element is a complex type
  TLN:=FXML.CreateElement('xs:complexType');
  N.AppendChild(TLN);
  // Complex type is a choice (0..Unbounded] of records
  N:=FXML.CreateElement('xs:choice');
  TLN.AppendChild(N);
  N['minOccurs']:='0';
  N['maxOccurs']:='unbounded';
  // Each record is an element
  E:=FXML.CreateElement('xs:element');
  N.AppendChild(E);
  E['name']:=Utf8Decode(RowName);
  // Record is a complex type of fields
  N:=FXML.CreateElement('xs:complexType');
  E.AppendChild(N);
  // Fields are a sequence. To this sequence, the fields may be appended.
  Result:=FXML.CreateElement('xs:sequence');
  N.AppendChild(Result);
end;

Const
  XMLPropTypeNames : Array [TRestFieldType] of string = (
   'unknown',          { rtfUnknown }
   'xs:int',          { rftInteger }
   'xs:int',          { rftLargeInt}
   'xs:double',       { rftFloat }
   'xs:dateTime',     { rftDate }
   'xs:dateTime',     { rftTime }
   'xs:dateTime',     { rftDateTime }
   'xs:string',       { rftString }
   'xs:boolean',      { rftBoolean }
   'xs:base64Binary'  { rftBlob }
  );

procedure TADOOutputStreamer.WriteMetadata(aFieldList: TRestFieldPairArray);

Var
  FMetadata : TDOMElement;
  F : TDomElement;
  P : TREstFieldPair;
  I : integer;
  S : Utf8String;
  K : TRestFieldType;

begin
  FMetadata:=CreateXSD;
  For I:=0 to Length(aFieldList)-1 do
    begin
    P:=aFieldList[i];
    K:=P.RestField.FieldType;
    S:=XMLPropTypeNames[K];
    F:=FXML.CreateElement('xs:element');
    F['name']:=Utf8Decode(P.Restfield.PublicName);
    F['type']:=Utf8decode(S);
    F['minOccurs']:='0';
    FMetaData.AppendChild(F);
    end;
end;

class function TADOOutputStreamer.GetContentType: String;
begin
  Result:='text/xml';
end;

function TADOOutputStreamer.RequireMetadata: Boolean;
begin
  Result:=True;
end;

procedure TADOOutputStreamer.CreateErrorContent(aCode: Integer; const aMessage: String);

Var
  ErrorObj : TDomElement;

begin
  ErrorObj:=FXML.CreateElement(UTF8Decode(GetString(rpErrorRoot)));
  ErrorObj['code']:=UTF8Decode(IntToStr(aCode));
  ErrorObj['message']:=UTF8Decode(aMessage);
  FRoot.AppendChild(ErrorObj);
end;

destructor TADOOutputStreamer.Destroy;
begin
  FreeAndNil(FXML);
  inherited Destroy;
end;

procedure TADOOutputStreamer.InitStreaming;

begin
  FXML:=TXMLDocument.Create;
  FXML.XMLStandalone:=True;
  if DataName='' then
    DataName:='Data';
  FRoot:=FXML.CreateElement('Data');
  FXML.AppendChild(FRoot);
  if RowName='' then
    RowName:='Row';
end;

Initialization
  TADOInputStreamer.RegisterStreamer('ado');
  TADOOutputStreamer.RegisterStreamer('ado');
end.

