{
    This file is part of the Free Component Library

    XML reading routines.
    Copyright (c) 1999-2000 by Sebastian Guenther, sg@freepascal.org
    Modified in 2006 by Sergei Gorelkin, sergei_gorelkin@mail.ru

    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.

 **********************************************************************}

{$IFNDEF FPC_DOTTEDUNITS}
unit XMLRead;
{$ENDIF FPC_DOTTEDUNITS}

{$ifdef fpc}
{$MODE objfpc}{$H+}
{$endif}

interface

{$IFDEF FPC_DOTTEDUNITS}
uses
  System.SysUtils, System.Classes, Xml.Dom, Xml.Utils, Xml.Reader, Xml.TextReader;
{$ELSE FPC_DOTTEDUNITS}
uses
  SysUtils, Classes, DOM, xmlutils, xmlreader, xmltextreader;
{$ENDIF FPC_DOTTEDUNITS}

type
  TErrorSeverity = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.TErrorSeverity;
  EXMLReadError = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.EXMLReadError;
  TXMLInputSource = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.TXMLInputSource;

const
  esWarning = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.esWarning;
  esError = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.esError;
  esFatal = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.esFatal;

procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); overload;
procedure ReadXMLFile(out ADoc: TXMLDocument; var f: Text); overload;
procedure ReadXMLFile(out ADoc: TXMLDocument; f: TStream); overload;
procedure ReadXMLFile(out ADoc: TXMLDocument; f: TStream; const ABaseURI: String); overload;

procedure ReadXMLFragment(AParentNode: TDOMNode; const AFilename: String); overload;
procedure ReadXMLFragment(AParentNode: TDOMNode; var f: Text); overload;
procedure ReadXMLFragment(AParentNode: TDOMNode; f: TStream); overload;
procedure ReadXMLFragment(AParentNode: TDOMNode; f: TStream; const ABaseURI: String); overload;

procedure ReadDTDFile(out ADoc: TXMLDocument; const AFilename: String);  overload;
procedure ReadDTDFile(out ADoc: TXMLDocument; var f: Text); overload;
procedure ReadDTDFile(out ADoc: TXMLDocument; f: TStream); overload;
procedure ReadDTDFile(out ADoc: TXMLDocument; f: TStream; const ABaseURI: String); overload;

type
  TXMLErrorEvent = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.TXMLErrorEvent;
  TDOMParseOptions = {$IFDEF FPC_DOTTEDUNITS}Xml.Reader{$ELSE}xmlreader{$ENDIF}.TXMLReaderSettings;

  // NOTE: DOM 3 LS ACTION_TYPE enumeration starts at 1
  TXMLContextAction = (
    xaAppendAsChildren = 1,
    xaReplaceChildren,
    xaInsertBefore,
    xaInsertAfter,
    xaReplace);

  TDOMParser = class(TObject)
  private
    FOptions: TDOMParseOptions;
    function GetOnError: TXMLErrorEvent;
    procedure SetOnError(value: TXMLErrorEvent);
  public
    constructor Create;
    destructor Destroy; override;
    procedure Parse(Src: TXMLInputSource; out ADoc: TXMLDocument);
    procedure ParseUri(const URI: XMLString; out ADoc: TXMLDocument);
    function ParseWithContext(Src: TXMLInputSource; Context: TDOMNode;
      Action: TXMLContextAction): TDOMNode;
    property Options: TDOMParseOptions read FOptions;
    property OnError: TXMLErrorEvent read GetOnError write SetOnError;
  end;

  TDecoder = {$IFDEF FPC_DOTTEDUNITS}Xml.TextReader{$ELSE}xmltextreader{$ENDIF}.TDecoder;
  TGetDecoderProc = {$IFDEF FPC_DOTTEDUNITS}Xml.TextReader{$ELSE}xmltextreader{$ENDIF}.TGetDecoderProc;

procedure RegisterDecoder(Proc: TGetDecoderProc);

// =======================================================

implementation

{$IFDEF FPC_DOTTEDUNITS}
uses
  Fcl.UriParser, Xml.DtdModel;
{$ELSE FPC_DOTTEDUNITS}
uses
  UriParser, dtdmodel;
{$ENDIF FPC_DOTTEDUNITS}

type
  TLoader = object
    doc: TDOMDocument;
    reader: TXMLTextReader;
    function CreateCDATANode(currnode: PNodeData): TDOMNode;
    function CreatePINode(currnode: PNodeData): TDOMNode;
    procedure ParseContent(cursor: TDOMNode_WithChildren);

    procedure ProcessXML(ADoc: TDOMDocument; AReader: TXMLTextReader);
    procedure ProcessFragment(AOwner: TDOMNode; AReader: TXMLTextReader);
    procedure ProcessDTD(ADoc: TDOMDocument; AReader: TXMLTextReader);
    procedure ProcessEntity(Sender: TXMLTextReader; AEntity: TEntityDecl);
  end;

procedure RegisterDecoder(Proc: TGetDecoderProc);
begin
  {$IFDEF FPC_DOTTEDUNITS}Xml.TextReader{$ELSE}xmltextreader{$ENDIF}.RegisterDecoder(Proc);
end;

{ TDOMParser }

constructor TDOMParser.Create;
begin
  FOptions := TDOMParseOptions.Create;
end;

destructor TDOMParser.Destroy;
begin
  FOptions.Free;
  inherited Destroy;
end;

function TDOMParser.GetOnError: TXMLErrorEvent;
begin
  result := Options.OnError;
end;

procedure TDOMParser.SetOnError(value: TXMLErrorEvent);
begin
  Options.OnError := value;
end;

procedure TDOMParser.Parse(Src: TXMLInputSource; out ADoc: TXMLDocument);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  ADoc := TXMLDocument.Create;
  Options.NameTable := ADoc.Names;
  Reader := TXMLTextReader.Create(Src, Options);
  try
    ldr.ProcessXML(ADoc, Reader);
  finally
    Reader.Free;
  end;
end;

procedure TDOMParser.ParseUri(const URI: XMLString; out ADoc: TXMLDocument);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  ADoc := TXMLDocument.Create;
  Options.NameTable := ADoc.Names;
  Reader := TXMLTextReader.Create(URI, Options);
  try
    ldr.ProcessXML(ADoc, Reader)
  finally
    Reader.Free;
  end;
end;

function TDOMParser.ParseWithContext(Src: TXMLInputSource;
  Context: TDOMNode; Action: TXMLContextAction): TDOMNode;
var
  Frag: TDOMDocumentFragment;
  node: TDOMNode;
  reader: TXMLTextReader;
  ldr: TLoader;
  doc: TDOMDocument;
begin
  if Action in [xaInsertBefore, xaInsertAfter, xaReplace] then
    node := Context.ParentNode
  else
    node := Context;
  // TODO: replacing document isn't yet supported
  if (Action = xaReplaceChildren) and (node.NodeType = DOCUMENT_NODE) then
    raise EDOMNotSupported.Create('DOMParser.ParseWithContext');

  if not (node.NodeType in [ELEMENT_NODE, DOCUMENT_FRAGMENT_NODE]) then
    raise EDOMHierarchyRequest.Create('DOMParser.ParseWithContext');

  if Context.NodeType = DOCUMENT_NODE then
    doc := TDOMDocument(Context)
  else
    doc := Context.OwnerDocument;

  Options.NameTable := doc.Names;
  reader := TXMLTextReader.Create(Src, Options);
  try
    Frag := doc.CreateDocumentFragment;
    try
      ldr.ProcessFragment(Frag, reader);
      Result := Frag.FirstChild;
      case Action of
        xaAppendAsChildren: Context.AppendChild(Frag);

        xaReplaceChildren: begin
          Context.TextContent := '';     // removes children
          Context.ReplaceChild(Frag, Context.FirstChild);
        end;
        xaInsertBefore: node.InsertBefore(Frag, Context);
        xaInsertAfter:  node.InsertBefore(Frag, Context.NextSibling);
        xaReplace:      node.ReplaceChild(Frag, Context);
      end;
    finally
      Frag.Free;
    end;
  finally
    reader.Free;
  end;
end;

procedure TLoader.ProcessXML(ADoc: TDOMDocument; AReader: TXMLTextReader);
begin
  doc := ADoc;
  reader := AReader;
  reader.OnEntity := @ProcessEntity;
  doc.documentURI := reader.BaseURI;
  reader.FragmentMode := False;
  ParseContent(doc);
  doc.XMLStandalone := reader.Standalone;

  if reader.Validate then
    reader.ValidateIdRefs;

  doc.IDs := reader.IDMap;
  reader.IDMap := nil;
end;

procedure TLoader.ProcessFragment(AOwner: TDOMNode; AReader: TXMLTextReader);
var
  DoctypeNode: TDOMDocumentType;
begin
  doc := AOwner.OwnerDocument;
  reader := AReader;
  reader.OnEntity := @ProcessEntity;
  reader.FragmentMode := True;
  reader.XML11 := doc.XMLVersion = '1.1';
  DoctypeNode := doc.DocType;
  if Assigned(DoctypeNode) then
    reader.DtdSchemaInfo := DocTypeNode.Model.Reference;
  ParseContent(aOwner as TDOMNode_WithChildren);
end;

procedure TLoader.ProcessEntity(Sender: TXMLTextReader; AEntity: TEntityDecl);
var
  DoctypeNode: TDOMDocumentType;
  Ent: TDOMEntity;
  src: TXMLCharSource;
  InnerReader: TXMLTextReader;
  InnerLoader: TLoader;
begin
  DoctypeNode := TDOMDocument(doc).DocType;
  if DoctypeNode = nil then
    Exit;
  Ent := TDOMEntity(DocTypeNode.Entities.GetNamedItem(AEntity.FName));
  if Ent = nil then
    Exit;
  Sender.EntityToSource(AEntity, Src);
  if Src = nil then
    Exit;
  InnerReader := TXMLTextReader.Create(Src, Sender);
  try
    Ent.SetReadOnly(False);
    InnerLoader.ProcessFragment(Ent, InnerReader);
    AEntity.FResolved := True;
  finally
    InnerReader.Free;
    AEntity.FOnStack := False;
    Ent.SetReadOnly(True);
  end;
end;

procedure TLoader.ParseContent(cursor: TDOMNode_WithChildren);
var
  element: TDOMElement;
  currnodeptr: PPNodeData;
  currnode: PNodeData;
begin
  currnodeptr := (reader as IGetNodeDataPtr).CurrentNodePtr;
  if reader.ReadState = rsInitial then
  begin
    if not reader.Read then
      Exit;
    case cursor.NodeType of
      DOCUMENT_NODE, ENTITY_NODE:
        (cursor as TDOMNode_TopLevel).SetHeaderData(reader.XMLVersion,reader.XMLEncoding);
    end;
  end;

  with reader do
  repeat
    if Validate then
      ValidateCurrentNode;

    currnode := currnodeptr^;
    case currnode^.FNodeType of
      ntText:
        cursor.InternalAppend(doc.CreateTextNodeBuf(currnode^.FValueStart, currnode^.FValueLength, False));

      ntWhitespace, ntSignificantWhitespace:
        if PreserveWhitespace then
          cursor.InternalAppend(doc.CreateTextNodeBuf(currnode^.FValueStart, currnode^.FValueLength, currnode^.FNodeType = ntWhitespace));

      ntCDATA:
        cursor.InternalAppend(CreateCDATANode(currnode));

      ntProcessingInstruction:
        cursor.InternalAppend(CreatePINode(currnode));

      ntComment:
        if not IgnoreComments then
          cursor.InternalAppend(doc.CreateCommentBuf(currnode^.FValueStart, currnode^.FValueLength));

      ntElement:
        begin
          element := LoadElement(doc, currnode, reader.AttributeCount);
          cursor.InternalAppend(element);
          cursor := element;
        end;

      ntEndElement:
          cursor := TDOMNode_WithChildren(cursor.ParentNode);

      ntDocumentType:
        cursor.InternalAppend(TDOMDocumentType.Create(doc, DtdSchemaInfo));

      ntEntityReference:
        begin
          cursor.InternalAppend(doc.CreateEntityReference(currnode^.FQName^.Key));
          { Seeing an entity reference while expanding means that the entity
            fails to expand. }
          if not ExpandEntities then
          begin
            { Make reader iterate through contents of the reference,
              to ensure correct validation events and character counts. }
            ResolveEntity;
            while currnodeptr^^.FNodeType <> ntEndEntity do
              Read;
          end;
        end;
    end;
  until not Read;
end;

function TLoader.CreatePINode(currnode: PNodeData): TDOMNode;
var
  s: DOMString;
begin
  SetString(s, currnode^.FValueStart, currnode^.FValueLength);
  result := Doc.CreateProcessingInstruction(currnode^.FQName^.Key, s);
end;

function TLoader.CreateCDATANode(currnode: PNodeData): TDOMNode;
var
  s: XMLString;
begin
  SetString(s, currnode^.FValueStart, currnode^.FValueLength);
  result := doc.CreateCDATASection(s);
end;



procedure TLoader.ProcessDTD(ADoc: TDOMDocument; AReader: TXMLTextReader);
begin
  AReader.DtdSchemaInfo := TDTDModel.Create(AReader.NameTable);
  // TODO: DTD labeled version 1.1 will be rejected - must set FXML11 flag
  doc.AppendChild(TDOMDocumentType.Create(doc, AReader.DtdSchemaInfo));
  AReader.ParseDTD;
end;

{ plain calls }

procedure ReadXMLFile(out ADoc: TXMLDocument; var f: Text);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  ADoc := TXMLDocument.Create;
  Reader := TXMLTextReader.Create(f, ADoc.Names);
  try
    ldr.ProcessXML(ADoc,Reader);
  finally
    Reader.Free;
  end;
end;

procedure ReadXMLFile(out ADoc: TXMLDocument; f: TStream; const ABaseURI: String);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  ADoc := TXMLDocument.Create;
  Reader := TXMLTextReader.Create(f, ABaseURI, ADoc.Names);
  try
    ldr.ProcessXML(ADoc, Reader);
  finally
    Reader.Free;
  end;
end;

procedure ReadXMLFile(out ADoc: TXMLDocument; f: TStream);
begin
  ReadXMLFile(ADoc, f, 'stream:');
end;

procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String);
var
  FileStream: TStream;
begin
  ADoc := nil;
  FileStream := TFileStream.Create(AFilename, fmOpenRead+fmShareDenyWrite);
  try
    ReadXMLFile(ADoc, FileStream, FilenameToURI(AFilename));
  finally
    FileStream.Free;
  end;
end;

procedure ReadXMLFragment(AParentNode: TDOMNode; var f: Text);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  Reader := TXMLTextReader.Create(f, AParentNode.OwnerDocument.Names);
  try
    ldr.ProcessFragment(AParentNode, Reader);
  finally
    Reader.Free;
  end;
end;

procedure ReadXMLFragment(AParentNode: TDOMNode; f: TStream; const ABaseURI: String);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  Reader := TXMLTextReader.Create(f, ABaseURI, AParentNode.OwnerDocument.Names);
  try
    ldr.ProcessFragment(AParentNode, Reader);
  finally
    Reader.Free;
  end;
end;

procedure ReadXMLFragment(AParentNode: TDOMNode; f: TStream);
begin
  ReadXMLFragment(AParentNode, f, 'stream:');
end;

procedure ReadXMLFragment(AParentNode: TDOMNode; const AFilename: String);
var
  Stream: TStream;
begin
  Stream := TFileStream.Create(AFilename, fmOpenRead+fmShareDenyWrite);
  try
    ReadXMLFragment(AParentNode, Stream, FilenameToURI(AFilename));
  finally
    Stream.Free;
  end;
end;


procedure ReadDTDFile(out ADoc: TXMLDocument; var f: Text);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  ADoc := TXMLDocument.Create;
  Reader := TXMLTextReader.Create(f, ADoc.Names);
  try
    ldr.ProcessDTD(ADoc,Reader);
  finally
    Reader.Free;
  end;
end;

procedure ReadDTDFile(out ADoc: TXMLDocument; f: TStream; const ABaseURI: String);
var
  Reader: TXMLTextReader;
  ldr: TLoader;
begin
  ADoc := TXMLDocument.Create;
  Reader := TXMLTextReader.Create(f, ABaseURI, ADoc.Names);
  try
    ldr.ProcessDTD(ADoc,Reader);
  finally
    Reader.Free;
  end;
end;

procedure ReadDTDFile(out ADoc: TXMLDocument; f: TStream);
begin
  ReadDTDFile(ADoc, f, 'stream:');
end;

procedure ReadDTDFile(out ADoc: TXMLDocument; const AFilename: String);
var
  Stream: TStream;
begin
  ADoc := nil;
  Stream := TFileStream.Create(AFilename, fmOpenRead+fmShareDenyWrite);
  try
    ReadDTDFile(ADoc, Stream, FilenameToURI(AFilename));
  finally
    Stream.Free;
  end;
end;




end.
