
//////////////////////////////////////////////////
//  DB Access Components
//  Copyright © 1998-2007 Core Lab. All right reserved.
//  TDALoader
//////////////////////////////////////////////////

{$IFNDEF CLR}

{$I Dac.inc}

unit DALoader;
{$ENDIF}

interface

uses
{$IFDEF CLR}
  Variants,
{$ENDIF}
  Classes, SysUtils, DBAccess, DB, MemData;

type
  TDAColumnDataType = (ctString, ctDate, ctInteger, ctUInteger, ctFloat);

  TDAColumnClass = class of TDAColumn;

  TDAColumn = class (TCollectionItem)
  private
    FName: string;
    FFieldType: TFieldType;

  protected
    function GetDataType: TDAColumnDataType; virtual;
    procedure SetDataType(Value: TDAColumnDataType); virtual;
    procedure SetFieldType(Value: TFieldType); virtual;
    function GetDisplayName: string; override;
    property DataType: TDAColumnDataType read GetDataType write SetDataType;

  public
    constructor Create(Collection: TCollection); override;

  published
    property Name: string read FName write FName;
    property FieldType: TFieldType read FFieldType write SetFieldType default ftString;
  end;

  TDAColumns = class (TOwnedCollection)
  private
    function GetColumn(Index: integer): TDAColumn;
    procedure SetColumn(Index: integer; Value: TDAColumn);

  public
    property Items[Index: integer]: TDAColumn read GetColumn write SetColumn; default;
  end;

  TDALoader = class;

  TDAPutDataEvent = procedure (Sender: TDALoader) of object;
  TGetColumnDataEvent = procedure (Sender: TObject; Column: TDAColumn; Row: integer;
    var Value: variant; var IsEOF: boolean) of object;

  TDALoader = class (TComponent)
  private
    FTableName: string;

    FOnPutData: TDAPutDataEvent;
    FOnGetColumnData: TGetColumnDataEvent;

    procedure SetConnection(Value: TCustomDAConnection);
    procedure SetColumns(Value: TDAColumns);

    function IsColumnsStored: boolean;
    procedure CreateColumnsByFields(Fields: TFields);
  protected
    FColumns: TDAColumns;
    FConnection: TCustomDAConnection;

    FLastRow: integer;
    FDesignCreate: boolean;

    FSkipReadOnlyFieldDescs: boolean;

    procedure Loaded; override;

    procedure Notification(Component: TComponent; Operation: TOperation); override;
    procedure BeginConnection; virtual;
    procedure EndConnection; virtual;

    procedure CheckTableName;

    procedure Prepare; virtual;
    procedure Reset; virtual;
    procedure InternalPutData; virtual;
    procedure PutData; virtual;
    procedure DoLoad; virtual;
    procedure Finish; virtual;
    procedure SetTableName(Value: string); virtual;

    class function DAColumnClass: TDAColumnClass; virtual;

    function UsedConnection: TCustomDAConnection; virtual;

    function ConvertDataTypeToColumnType( const DataType: word): TDAColumnDataType; virtual;
    procedure FillColumn(Column: TDAColumn; FieldDesc: TFieldDesc); virtual;
  public
    constructor Create(Owner: TComponent); override;
    destructor Destroy; override;

    procedure PutColumnData(Col: integer; Row: integer; const Value: variant); overload; virtual;
    procedure PutColumnData(const ColName: string; Row: integer; const Value: variant); overload;

    procedure Load; virtual;

    procedure CreateColumns;
    procedure LoadFromDataSet(DataSet: TDataSet);

    property Connection: TCustomDAConnection read FConnection write SetConnection;
    property TableName: string read FTableName write SetTableName;
    property Columns: TDAColumns read FColumns write SetColumns stored IsColumnsStored;

    property OnPutData: TDAPutDataEvent read FOnPutData write FOnPutData;
    property OnGetColumnData: TGetColumnDataEvent read FOnGetColumnData write FOnGetColumnData;
  end;

  TDALoaderUtils = class
  public
    class procedure SetDesignCreate(Obj: TDALoader; Value: boolean);
    class function GetDesignCreate(Obj: TDALoader): boolean;
    class function UsedConnection(Obj: TDALoader): TCustomDAConnection;
  end;

implementation

uses
{$IFDEF LINUX}
{$ELSE}
  Windows,
{$ENDIF}
  CRAccess, DAConsts {$IFNDEF CLR}{$IFDEF VER6P}, Variants{$ENDIF}{$ENDIF}, MemUtils;

{ TDAColumn }

constructor TDAColumn.Create(Collection: TCollection);
begin
  inherited;

  FFieldType := ftString;
end;

function TDAColumn.GetDataType: TDAColumnDataType;
begin
  case FieldType of
    ftString, ftMemo, ftFmtMemo, ftFixedChar, ftWideString, ftGuid {$IFDEF VER6P}, ftTimeStamp{$ENDIF}{$IFDEF VER10P}, ftWideMemo{$ENDIF}:
      Result := ctString;
    ftSmallint, ftInteger, ftWord, ftAutoInc:
      Result := ctInteger;
    ftLargeint:
      Result := ctUInteger;
    ftFloat, ftCurrency, ftBCD{$IFDEF VER6P}, ftFMTBcd{$ENDIF}:
      Result := ctFloat;
    ftDate, ftTime, ftDateTime:
      Result := ctDate;
    else
      Result := ctString;
  end;
end;

procedure TDAColumn.SetDataType(Value: TDAColumnDataType);
begin
  case Value of
    ctString:
      FieldType := ftString;
    ctDate:
      FieldType := ftDateTime;
    ctInteger:
      FieldType := ftInteger;
    ctUInteger:
      FieldType := ftLargeint;
    ctFloat:
      FieldType := ftFloat;
  end;
end;

procedure TDAColumn.SetFieldType(Value: TFieldType);
begin
  FFieldType := Value;
end;

function TDAColumn.GetDisplayName: string;
begin
  Result := FName;
end;

{ TDAColumns }

function TDAColumns.GetColumn(Index: integer): TDAColumn;
begin
  Result := TDAColumn(inherited Items[Index]);
end;

procedure TDAColumns.SetColumn(Index: integer; Value: TDAColumn);
begin
  Items[Index].Assign(Value);
end;

{ TDALoader }

constructor TDALoader.Create(Owner: TComponent);
begin
  inherited Create(Owner);

  FColumns := TDAColumns.Create(Self, DAColumnClass);

  FDesignCreate := csDesigning in ComponentState;
  FSkipReadOnlyFieldDescs := True;
end;

destructor TDALoader.Destroy;
begin
  FColumns.Free;

  inherited;
end;

procedure TDALoader.Loaded;
begin
  inherited;

  FDesignCreate := False;
end;

procedure TDALoader.BeginConnection;
begin
  if UsedConnection = nil then
    raise Exception.Create(SConnectionNotDefined);
  TDBAccessUtils.InternalConnect(UsedConnection);
end;

procedure TDALoader.EndConnection;
begin
  TDBAccessUtils.InternalDisconnect(UsedConnection);
end;

procedure TDALoader.CheckTableName;
begin
  if Trim(FTableName) = '' then
    raise Exception.Create(STableNameNotDefined);
end;

procedure TDALoader.Prepare;
begin
  BeginConnection;
  CheckTableName;
  Reset;
end;

procedure TDALoader.Finish;
begin
  EndConnection;
end;

procedure TDALoader.Reset;
begin
  FLastRow := -1;
end;

procedure TDALoader.PutColumnData(Col: integer; Row: integer; const Value: variant);
begin
  if Col >= FColumns.Count then
    DatabaseError('Invalid column number');

  if (Row < FLastRow) or (Row < 1) then
    DatabaseError('Invalid row number');

  FLastRow := Row - 1;
end;

procedure TDALoader.PutColumnData(const ColName: string; Row: integer; const Value: variant);
var
  i: integer;
begin
  for i := 0 to FColumns.Count - 1 do
    if AnsiSameText(ColName, FColumns[i].Name) then begin
      PutColumnData(i, Row, Value);
      Exit;
    end;
  raise Exception.Create(Format(SColumnNotFound, [ColName]));
end;

procedure TDALoader.InternalPutData;
var
  Value: variant;
  EOF: boolean;
  i,Row: integer;
begin
  if Assigned(FOnGetColumnData) then begin
    Row := 1;
    EOF := False;
    while not EOF do begin
      for i := 0 to FColumns.Count - 1 do begin
        FOnGetColumnData(Self, FColumns[i], Row, Value, EOF);
        if not EOF then
          PutColumnData(i, Row, Value)
        else begin
          if i <> 0 then
            FLastRow := -1; // to prevent insertion of incomplete row. If EOF is set to True on getting value 1..last field, all values of this record is ignored.
          break;            // stop loading immediately after getting EOF
        end;
      end;
      if not EOF then
        Inc(Row);
    end;
  end;
end;

procedure TDALoader.PutData;
begin
  if Assigned(FOnPutData) then
    FOnPutData(Self)
  else
    InternalPutData;
end;

procedure TDALoader.DoLoad;
begin
end;

procedure TDALoader.Load;
begin
  BeginConnection;
  try
    if Columns.Count = 0 then
      CreateColumns;
    try
      Prepare;
      StartWait;
      PutData;
    finally
      Finish;
      StopWait;
    end;
  finally
    EndConnection;
  end;
end;

function TDALoader.ConvertDataTypeToColumnType(const DataType: word): TDAColumnDataType;
begin
  Result := ctString; // To disable warning

  case DataType of
    dtUnknown, dtString, dtBoolean, dtBlob, dtMemo, dtWideMemo, dtExtString, dtWideString, dtExtWideString:
      Result := ctString;
    dtInt8, dtInt16, dtInt32, dtInt64:
      Result := ctInteger;
    dtUInt16, dtUInt32:
      Result := ctUInteger;
    dtFloat, dtCurrency:
      Result := ctFloat;
    dtDate, dtTime, dtDateTime:
      Result := ctDate;
  else
    //Assert(False, 'Unknown datatype (' + IntToStr(DataType) + ')');
  end;
end;

procedure TDALoader.FillColumn(Column: TDAColumn; FieldDesc: TFieldDesc);
begin
  Column.Name := FieldDesc.Name;
  Column.DataType := ConvertDataTypeToColumnType(FieldDesc.DataType);
  // Needs to override to fill ColumnType
end;

procedure TDALoader.CreateColumns;
var
  RecordSet: TCustomDADataSet;
  FieldDesc: TFieldDesc;
  i: integer;
begin
  BeginConnection;
  try
    CheckTableName;
    FColumns.Clear;
    RecordSet := UsedConnection.CreateDataSet;
    RecordSet.SQL.Text := 'SELECT * FROM ' + FTableName + ' WHERE 1=0'; // CR-M15322
    FColumns.BeginUpdate;
    try
      RecordSet.Execute;
      for i := 0 to RecordSet.FieldCount - 1 do begin
        FieldDesc := RecordSet.GetFieldDesc(RecordSet.Fields[i]);
        if not (FieldDesc.ReadOnly and FSkipReadOnlyFieldDescs) then
          FillColumn(TDAColumn(FColumns.Add), FieldDesc);
      end;
    finally
      FColumns.EndUpdate;
      RecordSet.Free;
    end;

  finally
    EndConnection;
  end;
end;

procedure TDALoader.CreateColumnsByFields(Fields: TFields);
var
  i: word;
  Field: TField;
begin
  FColumns.Clear;
  try
    FColumns.BeginUpdate;

    for i := 0 to Fields.Count - 1 do begin
      Field := Fields[i];
      if not Field.ReadOnly then
        with TDAColumn(FColumns.Add) do begin
          Name := Field.FieldName;
          FieldType := Field.DataType;
        end;
    end;

  finally
    FColumns.EndUpdate;
  end;
end;

procedure TDALoader.Notification(Component: TComponent; Operation: TOperation);
begin
  if (Component = FConnection) and (Operation = opRemove) then
    Connection := nil;

  inherited;
end;

procedure TDALoader.SetConnection(Value: TCustomDAConnection);
begin
  if Value <> FConnection then begin
    if FConnection <> nil then
      RemoveFreeNotification(FConnection);

    FConnection := Value;

    if FConnection <> nil then
      FreeNotification(FConnection);
  end;
end;

procedure TDALoader.SetColumns(Value: TDAColumns);
begin
  FColumns.Assign(Value);
end;

function TDALoader.IsColumnsStored: boolean;
begin
  Result := FColumns.Count > 0;
end;

procedure TDALoader.SetTableName(Value: string);
begin
  if Value <> FTableName then begin
    FTableName := Value;
    if not (csLoading in ComponentState) and (UsedConnection <> nil) and UsedConnection.Connected and (FColumns.Count = 0) then
      CreateColumns;
  end;
end;

class function TDALoader.DAColumnClass: TDAColumnClass;
begin
  Result := TDAColumn;
end;

function TDALoader.UsedConnection: TCustomDAConnection;
begin
  Result := FConnection;
end;

procedure TDALoader.LoadFromDataSet(DataSet: TDataSet);
var
  row, col: integer;
  ColNo: array of integer;
  OldActive: boolean;

  Field: TField;
  FieldDesc: TFieldDesc;
  RecordSet: TCRRecordSet;
  ObjRef: TSharedObject;
  IsBlank: boolean;
  AValue: variant;
  Bookmark: TBookmark;

  procedure FillColumsNumber;
  var
    i, j: integer;
    fname: string;
  begin
    for i := 0 to DataSet.FieldCount - 1 do begin
      ColNo[i] := -1;
      fname := DataSet.Fields[i].FieldName;

      for j := 0 to FColumns.Count - 1 do
        if AnsiSameText(fname, FColumns[j].Name) then begin
          ColNo[i] := j;
          break;
        end;
    end;
  end;

begin
  if DataSet = nil then
    raise Exception.Create(SDataSetNotDefined);

  OldActive := DataSet.Active;
  Bookmark := nil;
  try
    DataSet.DisableControls;
    Bookmark := DataSet.GetBookmark;

    DataSet.Open;
    DataSet.First;

    SetLength(ColNo, DataSet.FieldCount);

    if Columns.Count = 0 then begin
      CreateColumnsByFields(DataSet.Fields);

      for col := 0 to DataSet.FieldCount - 1 do
        ColNo[col] := col;
    end
    else
      FillColumsNumber;

    Prepare;
    StartWait;
    try
      DataSet.First;
      for row := 1 to DataSet.RecordCount do begin
        for col := 0 to DataSet.FieldCount - 1 do
          if ColNo[col] >= 0 then begin
            Field := DataSet.Fields[col];
            if DataSet is TCustomDADataSet then
              with TCustomDADataSet(DataSet) do begin
                FieldDesc := GetFieldDesc(Field);
                if FieldDesc <> nil then begin
                  RecordSet := TDBAccessUtils.GetIRecordSet(TCustomDADataSet(DataSet));
                  if RecordSet.IsComplexFieldType(FieldDesc.DataType)
                    and not((FieldDesc.DataType = dtExtString)
                    or (FieldDesc.DataType = dtExtWideString)
                    or (FieldDesc.DataType = dtExtVarBytes)
                    {$IFDEF VER5P}or (FieldDesc.DataType = dtVariant){$ENDIF}) then begin
                    IsBlank := RecordSet.GetNull(FieldDesc.FieldNo, ActiveBuffer);
                    ObjRef := RecordSet.GetObject(FieldDesc.FieldNo, ActiveBuffer);
                    if IsBlank then
                      AValue := Null
                    else begin
                    {$IFDEF CLR}
                      AValue := Variant(ObjRef);
                    {$ELSE}
                      AValue := Unassigned;
                      TVarData(AValue).VType := varByRef;
                      TVarData(AValue).VPointer := ObjRef;
                    {$ENDIF}
                    end;
                    PutColumnData(ColNo[col], row, AValue);
                    Continue;
                  end;
                end;
              end;
            // To avoid memory leak
            AValue := Unassigned;
            AValue := Field.Value;
            PutColumnData(ColNo[col], row, AValue);
          end;
        DataSet.Next;
      end;
      DoLoad;
    finally
      Finish;
      StopWait;
    end;
  finally
    DataSet.Active := OldActive;
    DataSet.GotoBookmark(Bookmark);
    DataSet.EnableControls;
  end;
end;

{ TDALoaderUtils }

class procedure TDALoaderUtils.SetDesignCreate(Obj: TDALoader; Value: boolean);
begin
  Obj.FDesignCreate := Value;
end;

class function TDALoaderUtils.GetDesignCreate(Obj: TDALoader): boolean;
begin
  Result := Obj.FDesignCreate;
end;

class function TDALoaderUtils.UsedConnection(Obj: TDALoader): TCustomDAConnection;
begin
  Result := Obj.UsedConnection;
end;

end.
