unit SynDBExplorerMain;

{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

{.$define USEZEOS}

uses
  Windows,
  Messages,
  SysUtils,
  Variants,
  Classes,
  Graphics,
  Controls,
  Forms,
  Grids,
  ExtCtrls,
  StdCtrls,
  {$ifndef FPC}
  Consts,
  {$ifdef HASINLINE}  XPMan,
  Contnrs, {$endif}
  {$endif}
  {$ifdef ISDELPHIXE}
  SynSQLite3RegEx, // use direct PCRE library as available since Delphi XE
  {$endif}
  SynCommons,
  SynZip,
  mORMot,
  SynSQLite3,
  SynSQLite3Static,
  mORMotUI,
  mORMotUIEdit,
  mORMotUILogin,
  mORMotToolBar,
  SynTaskDialog, // also fix QC 37403 for Delphi 6/7/2006
  SynTable, // for TSynFilter and TSynValidate
  SynDB,
  SynDBOracle,
  SynOleDB,
  SynDBSQLite3,
  SynDBODBC,
  SynDBRemote,
  {$ifdef USEZEOS}
  SynDBZeos,
  {$endif}
  SynDBExplorerClasses,
  SynDBExplorerFrame,
  ComCtrls;

type
  TDbExplorerMain = class(TForm)
    procedure FormDestroy(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
  private
    MainCaption: string;
    Connection: TExpConnectionType;
    Page: TSynPager;
    PageNew: TSynPage;
    TempFileName: TFileName;
    procedure PageChange(Sender: TObject);
    procedure PageDblClick(Sender: TObject);
  public
    ConnectionName: string;
    Tables: TStringList;
    Props: TSQLDBConnectionProperties;
    function CreateFrame: TDBExplorerFrame;
  end;

var
  DbExplorerMain: TDbExplorerMain;

  
resourcestring
  sSelectAConnection = 'Select a connection';
  sNew = 'New Connection';
  sNewOne = 'New';
  sConnectionHints = 'Display name|Database type|Server name '+
    '(for "Generic OLEDB", use ADO-like connection string, and ignore other fields; '+
    'for SQLite3 or Jet, specify the full file name)|'+
    'Database name (unneeded for Oracle/SQLite3/Jet/ODBC)|User login|'+
    'User password (set ? for prompt)';
  sSelectOrCreateAConnection = 'Select a connection to be used, or\n'+
    'click on "New connection" to create one.';
  sPleaseWaitN = 'Connecting to %s...';
  sPleaseWaitSynLz = 'Decompressing %s...';
  sUpdateConnection = 'Update connection settings';
  sPassPromptN = 'Please enter password for %s@%s:';


implementation

uses
  SynDBExplorerServer;


{$ifndef HASINLINE}
  {$R vista.RES} // includes Win10 manifest - use .RES for linux cross-compilation
{$endif}

{$R *.dfm}

procedure TDbExplorerMain.FormDestroy(Sender: TObject);
begin
  Tables.Free;
  Props.Free;
  if TempFileName<>'' then
    DeleteFile(TempFileName);
end;

function Crypt(const s: RawUTF8): RawUTF8;
var i: integer;
begin // just not to be written in plain ascii in .config file
  SetLength(result,length(s));
  for i := 0 to length(s)-1 do
    PByteArray(result)[i] := PByteArray(s)[i] xor (i+137);
end;

procedure TDbExplorerMain.FormCreate(Sender: TObject);
var Conns: TSQLRestStorageInMemory;
function TryConnect(C: TSQLConnection; LoadTableNames: boolean): boolean;
const CONN_CLASSES: array[TExpConnectionType] of TSQLDBConnectionPropertiesClass =
  (TSQLDBOracleConnectionProperties,
   TOleDBOracleConnectionProperties,TOleDBMSOracleConnectionProperties,
   TOleDBMSSQLConnectionProperties,TOleDBConnectionProperties,
   TSQLDBSQLite3ConnectionProperties,
   {$ifdef WIN64}
   nil, // no JET/MSAccess available under Win64
   {$else}
   TOleDBJetConnectionProperties,
   {$endif}
   TODBCConnectionProperties,
   TSQLDBWinHTTPConnectionProperties,
   {$ifdef USEZEOS}TSQLDBZeosConnectionProperties{$else}nil{$endif}
  );
var i: integer;
    Pass: RawUTF8;
begin
  result := false;
  if CONN_CLASSES[C.Connection]=nil then begin
    {$ifndef USEZEOS}
    if C.Connection=ctZEOS then
      ShowMessage('USEZEOS conditional should be defined in SynDBExplorerMain.pas',
        'Zeos/ZDBC not available',true);
    {$endif} 
    exit;
  end;
  try
    Pass := Crypt(C.Password);
    if Pass='?' then 
      Pass := StringToUTF8(InputBox(Caption,format(sPassPromptN,
        [UTF8ToString(C.UserName),UTF8ToString(C.Server)]),'',true));
    if C.Connection=ctGenericOLEDB then begin
      Props := TOleDBConnectionProperties.Create('','','','');
      with TOleDBConnectionProperties(Props) do begin
        ConnectionString := UTF8ToWideString(C.Server);
        if LoadTableNames then begin
          ConnectionStringDialogExecute(Handle);
          C.Server := WideStringToUTF8(ConnectionString);
        end;
      end;
    end else 
      Props := CONN_CLASSES[C.Connection].Create(C.Server,C.Database,C.UserName,Pass);
    ConnectionName := UTF8ToString(C.Ident);
    with CreateTempForm(format(sPleaseWaitN,[ConnectionName]),nil,True) do
    try
      Connection := C.Connection;
      MainCaption := format('%s %s (compiled with %s) - %s',
        [MainCaption,SYNOPSE_FRAMEWORK_VERSION,GetDelphiCompilerVersion,ConnectionName]);
      if LoadTableNames or                      // retrieve all needed info from DB
         (C.Connection=ctRemoteHTTP) then begin
        Props.GetTableNames(C.fTableNames);     // retrieve and set table names
        C.ForeignKeys := CompressString(Props.ForeignKeysData); // foreign keys
        if Conns<>nil then
          Conns.Modified := true;
      end else begin
        Props.ThreadSafeConnection.Connect;
        Props.ForeignKeysData := UncompressString(C.ForeignKeys); 
      end;
      for i := 0 to High(C.TableNames) do
        Tables.Add(UTF8ToString(C.TableNames[i]));
{$ifdef ISDELPHIXE}
     with TSQLDBSQLite3Connection(Props.ThreadSafeConnection) do
     if InheritsFrom(TSQLDBSQLite3Connection) then
       CreateRegExpFunction(DB.DB);
{$endif}
      result := true;
    finally
      Screen.Cursor := crDefault;
      Free;
    end;
  except
    on E: Exception do begin
      ShowMessage(E.Message,true);
      FreeAndNil(Props);
    end;
  end;
end;
var Btns: TCommonButtons;
    Task: TTaskDialog;
    C: TSQLConnection;
    CmdLine: TExpConnectionType;
    FN, msg, FN2: string;
    i, res: Integer;
    tmp: array[0..MAX_PATH] of char;
begin
  Conns := nil;
  DefaultFont.Name := 'Tahoma';
  DefaultFont.Size := 9;
  Tables := TStringList.Create;
  C := nil;
  with TSQLConnection.RecordProps do begin
    AddFilterOrValidate('Ident',TSynFilterTrim.Create);
    AddFilterOrValidate('Ident',TSynValidateText.Create);
    //AddFilterOrValidate('Server',TSynValidateText.Create);
  end;
  MainCaption := Caption;
  if (ParamCount=1) and FileExists(paramstr(1)) then begin
    FN := paramstr(1);
    CmdLine := ctOracleDirectOCI;
    if IsJetFile(FN) then
      CmdLine := ctJet_mdbOLEDB else
    if IsSQLite3File(FN) then
      CmdLine := ctSqlite3 else
    if TSQLDataBase.IsBackupSynLZFile(FN) then begin
      FN2 := ExtractFileName(FN);
      SetString(TempFileName, tmp, GetTempPath(SizeOf(tmp), tmp));
      TempFileName := TempFileName+FN2+'.db';
      DeleteFile(TempFileName);
      with CreateTempForm(format(sPleaseWaitSynLz,[FN2]),nil,True) do
      try
        if TSQLDatabase.BackupUnSynLZ(FN, TempFileName) then begin
          CmdLine := ctSqlite3;
          FN := TempFileName;
        end;
      finally
        Screen.Cursor := crDefault;
        Free;
      end;
    end;
    if CmdLine=ctOracleDirectOCI then begin
      ShowMessage(FN+'?',True);
      exit;
    end;
    C := TSQLConnection.Create;
    try
      C.Connection := CmdLine;
      C.Ident := StringToUTF8(FN);
      C.Server := C.Ident;
      TryConnect(C,True);
    finally
      C.Free;
    end;
  end else begin
    Conns := TSQLRestStorageInMemory.Create(
      TSQLConnection,nil,ChangeFileExt(ExeVersion.ProgramFileName,'.config'),false);
    try
      Conns.ExpandedJSON := true; // for better human reading and modification
      Task.Title := MainCaption;
      Task.Inst := sSelectAConnection;
      Task.Content := sSelectOrCreateAConnection;
      if Conns.Count=0 then
        Btns := [cbCancel] else begin
        for i := 0 to Conns.Count-1 do
          Task.Selection := Task.Selection+UTF8ToString(TSQLConnection(Conns[i]).Ident)+#10;
        Btns := [cbOk,cbCancel];
        Task.Query := UTF8ToString(TSQLConnection(Conns[0]).Ident);
        Task.Verify := sUpdateConnection;
      end;
      Task.VerifyChecked := false;
      Task.Buttons := sNew;
      res := Task.Execute(Btns,0,[],tiQuestion);
      case res of
      mrOk:
        if Task.VerifyChecked then begin
          C := TSQLConnection(Conns[Task.SelectionRes]);
          msg := Task.Verify;
        end else
          TryConnect(TSQLConnection(Conns[Task.SelectionRes]),false);
      mrBtn1: begin
        C := TSQLConnection.Create;
        msg := sNew;
      end;
      end;
      if C<>nil then
        with TRecordEditForm.Create(self) do
        try
          C.Password := Crypt(C.Password);
          SetRecord(nil,C,nil,nil,sConnectionHints,0,msg);
          if ShowModal=mrOk then begin
            C.Password := Crypt(C.Password);
            if TryConnect(C,true) and (res=mrBtn1) then
              Conns.AddOne(C,false,'') else
              if res=mrBtn1 then
                FreeAndNil(C);
          end else
            if res=mrBtn1 then
              FreeAndNil(C);
        finally
          Free;
        end;
    finally
      Conns.Free;
    end;
  end;
  Page := TSynPager.Create(self);
  Page.ControlStyle := Page.ControlStyle+[csClickEvents]; // enable OnDblClick
  Page.Parent := self;
  Page.Align := alClient;
  PageNew := TSynPage.Create(self);
  PageNew.Caption := sNewOne;
  PageNew.PageControl := Page;
  Page.OnChange := PageChange;
  Page.OnDblClick := PageDblClick;
end;

procedure TDbExplorerMain.FormShow(Sender: TObject);
begin
  if Props=nil then begin
    Close;
    exit;
  end;
  Caption := MainCaption;
  SetStyle(self);
  CreateFrame;
end;

procedure TDbExplorerMain.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  case Key of
    ord('T'):
      if Shift=[ssCtrl] then
        CreateFrame;
    VK_F9:
      with Page.ActivePage do
        if TObject(Tag).InheritsFrom(TDBExplorerFrame) then
          with TDBExplorerFrame(Tag) do
          if Shift=[] then
            BtnExecClick(BtnExec) else
          if ssShift in Shift then
            BtnExecClick(BtnExecToTab);
  end;
end;

var
  FrameID: integer;
  
function TDbExplorerMain.CreateFrame: TDBExplorerFrame;
var P: TSynPage;
begin
  P := TSynPage.Create(self);
  P.PageControl := Page;
  P.PageIndex := Page.PageCount-2;
  result := TDBExplorerFrame.Create(P);
  result.Parent := P;
  result.Align := alClient;
  result.Tables := Tables;
  result.Props := Props;
  result.EditTableChange(nil);
  inc(FrameID);
  result.Name := 'P'+IntToStr(FrameID);
  P.Tag := PtrInt(result);
  Page.ActivePage := P;
  result.EditTable.SetFocus;
  SetStyle(P);
end;

procedure TDbExplorerMain.PageChange(Sender: TObject);
begin
  if Page.ActivePage=PageNew then
    CreateFrame;
end;

procedure TDbExplorerMain.PageDblClick(Sender: TObject);
var n, i: Integer;
begin
  i := Page.ActivePageIndex;
  n := Page.PageCount-2;
  if n>0 then begin
    Page.ActivePage.Free;
    if i=n then
      Page.ActivePageIndex := n-1;
  end;
end;

end.
