{
    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.

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

unit XMLRead;

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

interface

uses
  SysUtils, Classes, DOM, xmlutils, xmlreader, xmltextreader;

type
  TErrorSeverity = xmlreader.TErrorSeverity;
  EXMLReadError = xmlreader.EXMLReadError;
  TXMLInputSource = xmlreader.TXMLInputSource;

const
  esWarning = xmlreader.esWarning;
  esError = xmlreader.esError;
  esFatal = xmlreader.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 = xmlreader.TXMLErrorEvent;
  TDOMParseOptions = xmlreader.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 = xmltextreader.TDecoder;
  TGetDecoderProc = xmltextreader.TGetDecoderProc;

procedure RegisterDecoder(Proc: TGetDecoderProc);

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

implementation

uses
  UriParser, dtdmodel;

type
  TDOMDocumentTypeEx = class(TDOMDocumentType);
  TDOMTopNodeEx = class(TDOMNode_TopLevel);

  TDOMEntityEx = class(TDOMEntity);

  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
  xmltextreader.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: TDOMDocumentTypeEx;
begin
  doc := AOwner.OwnerDocument;
  reader := AReader;
  reader.OnEntity := @ProcessEntity;
  reader.FragmentMode := True;
  reader.XML11 := doc.XMLVersion = '1.1';
  DoctypeNode := TDOMDocumentTypeEx(doc.DocType);
  if Assigned(DoctypeNode) then
    reader.DtdSchemaInfo := DocTypeNode.FModel.Reference;
  ParseContent(aOwner as TDOMNode_WithChildren);
end;

procedure TLoader.ProcessEntity(Sender: TXMLTextReader; AEntity: TEntityDecl);
var
  DoctypeNode: TDOMDocumentType;
  Ent: TDOMEntityEx;
  src: TXMLCharSource;
  InnerReader: TXMLTextReader;
  InnerLoader: TLoader;
begin
  DoctypeNode := TDOMDocument(doc).DocType;
  if DoctypeNode = nil then
    Exit;
  Ent := TDOMEntityEx(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;
    if cursor is TDOMNode_TopLevel then
    begin
      if reader.XMLVersion <> xmlVersionUnknown then
        TDOMTopNodeEx(cursor).FXMLVersion := reader.XMLVersion;
      TDOMTopNodeEx(cursor).FXMLEncoding := 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.
