// sqlsrv.cpp
// 
// Copyright (c) 2010 Accenture. All rights reserved.
// This component and the accompanying materials are made available
// under the terms of the "Eclipse Public License v1.0"
// which accompanies this distribution, and is available
// at the URL "http://www.eclipse.org/legal/epl-v10.html".
// 
// Initial Contributors:
// Accenture - Initial contribution
//
#include "sqlsrv.h"

#include "sqlcmd_open.h"

_LIT(KNewLine, "\r\n");
_LIT(KPrompt, "ok\r\n");

_LIT(KHistoryFile, "c:\\system\\console\\fshell_sqlsrv\\history");

const TInt KFileBufferSize = 32768;

CCmdSqlSrv*	CCmdSqlSrv::iOnlyInstance = NULL;

CCommandBase* CCmdSqlSrv::NewLC()
	{
	if (CCmdSqlSrv::iOnlyInstance)
		{
		User::LeaveIfError(KErrAlreadyExists);
		}
	
	CCmdSqlSrv* self = new (ELeave)CCmdSqlSrv();
	CCmdSqlSrv::iOnlyInstance = self;
	CleanupStack::PushL(self);
	self->BaseConstructL();
	return self;
	}

//static:
//Check if a string is wrapped by double quote, and removed them
//
void CCmdSqlSrv::StripWrapDoubleQuote(HBufC& aText)
	{
	RBuf text;
	text.Create(aText);
	text.Trim();
	if (text.Left(1)==_L("\"") && text.Right(1)== _L("\""))
		{
		//delete right side quote
		text.Delete(text.Length()-1, 1);
		//delete left side quote
		text.Delete(0,1);
		aText = text;
		}
	text.Close();
	}



//static 
CCmdSqlSrv* CCmdSqlSrv::GetServer()
	{
	return CCmdSqlSrv::iOnlyInstance;	
	}

CCmdSqlSrv::CCmdSqlSrv()
	: CServerBase(0, KPrompt, KHistoryFile), iNextCommandId(1)
	{
	iTmpFileNo = 1;
	iDatabaseOpened = EFalse;
	}

void CCmdSqlSrv::ConstructL()
	{
	BaseConstructL();
	}

CCmdSqlSrv::~CCmdSqlSrv()
	{
	iSqlDb.Close();	
	CCmdSqlSrv::iOnlyInstance = NULL;
	}

//////////////////////////////////////////////////////


//////////////////////////////////////////////////////

const TDesC& CCmdSqlSrv::Name() const
	{
	_LIT(KName, "sqlsrv");
	return KName;
	}

const TDesC& CCmdSqlSrv::Description() const
	{
	_LIT(KDescription, "a command to interact with Symbian SQLite server");
	return KDescription;
	}

void CCmdSqlSrv::ArgumentsL(RCommandArgumentList& /*aArguments*/)
	{
	}

void CCmdSqlSrv::OptionsL(RCommandOptionList& /*aOptions*/)
	{
	}

TInt CCmdSqlSrv::NextCommandId()
	{
	return iNextCommandId++;
	}

//when retrieving SQL records, if there is any binary columns and user has specified
//a Temp file template, then we will generate a temp file and dump the binary into such file.
void CCmdSqlSrv::MakeTempFilename(TDes& aTmpFile, TDesC& aTemplate)
	{
	TFileName2 tmpTemplate(aTemplate);
	TPtrC drvPath = tmpTemplate.DriveAndPath();
	TPtrC name = tmpTemplate.Name();
	TPtrC ext = tmpTemplate.Ext();
	
	aTmpFile.Format(_L("%S%S.%02d%S"), &drvPath, &name, iTmpFileNo, &ext);
	iTmpFileNo++;
	}


void CCmdSqlSrv::ReportResult(const TServerCommandId& aId, const TDesC& aName, TRefByValue<const TDesC> aFmt, ...)
	{
	CheckNewConsoleLine();
	DoPrintf(_L("result:%u:%S:"), aId.Value(), &aName);
	VA_LIST list;
	VA_START(list, aFmt);
	DoPrintList(aFmt, list, ENewLine);
	VA_END(list);
	}


void CCmdSqlSrv::InitializeL()
	{
	CServerCommandFactory& factory = Factory();
	
	factory.AddLeafCommandL<CSqlCmdExit>();
	factory.AddLeafCommandL<CSqlCmdOpen>();
	factory.AddLeafCommandL<CSqlCmdCreate>();
	factory.AddLeafCommandL<CSqlCmdExec>();
	factory.AddLeafCommandL<CSqlCmdClose>();	
	factory.AddLeafCommandL<CSqlCmdState>();
	factory.AddLeafCommandL<CSqlCmdAttach>();
	factory.AddLeafCommandL<CSqlCmdDetach>();
	
#ifdef SQL_COMPACT	
	factory.AddLeafCommandL<CSqlCmdCompact>();
#endif
		
	}

void CCmdSqlSrv::DoPrintf(TRefByValue<const TDesC> aFmt, ...)
	{
	VA_LIST list;
	VA_START(list, aFmt);
	DoPrintList(aFmt, list);
	VA_END(list);
	}

void CCmdSqlSrv::DoPrintf(TRefByValue<const TDesC8> aFmt, ...)
	{
	VA_LIST list;
	VA_START(list, aFmt);
	DoPrintList(aFmt, list);
	VA_END(list);
	}

void CCmdSqlSrv::DoPrintList(TRefByValue<const TDesC> aFmt, VA_LIST& aList, TPrintPostfix aPostfix)
	{
	TOverflowTruncate overflow;
	TBuf<0x100> buf;
	buf.AppendFormatList(aFmt, aList, &overflow);
	DoPrint(buf, aPostfix);
	}

void CCmdSqlSrv::DoPrintList(TRefByValue<const TDesC8> aFmt, VA_LIST& aList, TPrintPostfix aPostfix)
	{
	TOverflowTruncate8 overflow;
	TBuf8<0x200> buf;
	buf.AppendFormatList(aFmt, aList, &overflow);
	if (buf.Length() > 0x100) buf.SetLength(0x100); // Truncate to half the buffer size so that the call to Expand doesn't panic
	TPtrC wideBuf = buf.Expand();
	DoPrint(wideBuf, aPostfix);
	}

void CCmdSqlSrv::DoPrint(const TDesC& aDes, TPrintPostfix aPostfix)
	{
	Stdout().Write(aDes);
	if ((aPostfix == ENewLine) && ((aDes.Length() < KNewLine().Length()) || (aDes.Right(KNewLine().Length()) != KNewLine)))
		{
		Stdout().Write(KNewLine);
		}
	}


void CCmdSqlSrv::Report(const TServerCommandId& /*aId*/, const TDesC& /*aDes*/)
	{
	//TODO?
//	Log(aId, aDes);
	}

void CCmdSqlSrv::Report(const TServerCommandId& /*aId*/, const TDesC8& /*aDes*/)
	{
	//TODO?
//	Log(aId, aDes);
	}

void CCmdSqlSrv::ReportWarning(const TServerCommandId& aId, const TDesC& aDes)
	{
	CheckNewConsoleLine();
	DoPrintf(_L("warning:%u:%S\r\n"), aId.Value(), &aDes);
	}

void CCmdSqlSrv::ReportError(const TServerCommandId& aId, TInt aError, const TDesC& aDes)
	{
	CheckNewConsoleLine();
	DoPrintf(_L("error:%d:%u:%S\r\n"), aError, aId.Value(), &aDes);
	}

////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////

//aCommand: the command which is invoking this function, SHOULD NOT BE NULL
//
void CCmdSqlSrv::SqlCreateL(TDesC& aFilename, CServerCommandBase* aCommand)
	{
	if (iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("A database is already opened"));
	
	iSqlDb.CreateL(aFilename);
	
	//Print size information if no error occurrs
	TInt size = iSqlDb.Size();
	ReportResult(aCommand->Id(), aCommand->Name(), _L("Database created, size: %d bytes.\r\n"), size);
	iDatabaseOpened = ETrue;
	}

void CCmdSqlSrv::SqlOpenL(TDesC& aFilename, CServerCommandBase* aCommand)
	{
	if (iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("A database is already opened"));
		
	iSqlDb.OpenL(aFilename);
	
	//Print size information if no error occurrs
	TInt size = iSqlDb.Size();
	ReportResult(aCommand->Id(), aCommand->Name(), _L("Database opened, size: %d bytes.\r\n"), size);
	iDatabaseOpened = ETrue;

	//more detailed size information seems only available from Symbian OS 9.5 or later.
/*	
	RSqlDatabase::TSize size;
	iSqlDb.Size(size);
	ReportResult(aCommand->Id(), aCommand->Name(), _L("Database opened, size: %Ld bytes. free:%Ld bytes\r\n"), 
			size.iSize, size.iFree);
*/			
	}

//execute a SQL statement without parameter/response
void CCmdSqlSrv::SqlExecL(TDesC& aStatement, CServerCommandBase* aCommand)
	{
	TInt err;
	if (!iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("No database opened"));
		
	err = iSqlDb.Exec(aStatement);
	
	if (err < 0)
		{
		TPtrC errMsg = iSqlDb.LastErrorMessage();
		ReportResult(aCommand->Id(), aCommand->Name(), _L("Exec return code:%d message:%S\r\n"),
				err, &errMsg);				
		}
	
	User::LeaveIfError(err);
	
	//print the return code if it's not error
	ReportResult(aCommand->Id(), aCommand->Name(), _L("Exec return code:%d\r\n"),err);		
	}

//execute a SQL statement with parameter/response
//aStatement: SQL statement
//aOptParamFile: contains multiple string (HBufC*):should be parameters for that statement
//aTempFileTemplate: template to generate temp file, used to dump binary contents
void CCmdSqlSrv::SqlStateL(TDesC& aStatement, RPointerArray<HBufC> &aOptParamFile, CServerCommandBase* aCommand, TDesC* aTempFileTemplate)
	{
	TInt err;
	if (!iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("No database opened"));
	
	RSqlStatement sqlState;
	CleanupClosePushL(sqlState);
	
	err = sqlState.Prepare(iSqlDb, aStatement);
	if (err < 0)
		{
		TPtrC errMsg = iSqlDb.LastErrorMessage();
		ReportResult(aCommand->Id(), aCommand->Name(), _L("RSqlStatement::Prepare() return code:%d message:%S\r\n"),
				err, &errMsg);				
		}
	User::LeaveIfError(err);

	//depend on the statement type, for something like SELECT, which is expected to have returned results,
	//we should call Next(),
	//otherwise we should call Exec();
	//Panic will occur if calling wrong function
	//so check if this statement is SELECT
	TBool bIsSelect;
	TBuf<64> strBegin;
	strBegin.Copy(aStatement.Left(64));
	strBegin.TrimLeft();
	strBegin.UpperCase();
	
	if (strBegin.Left(6)==_L("SELECT"))
		bIsSelect = ETrue;
	else
		bIsSelect = EFalse;
	
	if (bIsSelect) 
		{
		//once successfully execute the statement, check if there is any response.
		//only quit the loop when there is no more results found
		TInt totalRecordsReturned = 0;	
		while(ETrue)
			{	
			err =  sqlState.Next();
			if (err==KSqlAtRow)	//ready to retrieve a result (usual response for SELECT command)
				{
				totalRecordsReturned++;
				ReportResult(aCommand->Id(), aCommand->Name(), _L("Record #%d =========================\r\n"), totalRecordsReturned);
				
				ParseResultL(sqlState, aCommand, aTempFileTemplate);
					
				}
			else if (err==KSqlAtEnd) //no records found
				{
				ReportResult(aCommand->Id(), aCommand->Name(), _L("===================================\r\n"));
				ReportResult(aCommand->Id(), aCommand->Name(), _L("%d record(s) returned\r\n"), totalRecordsReturned);
				break;
				}
			else 
				{
				User::LeaveIfError(err);			
				}
			}
		}
	else
		{		
		RSqlParamWriteStream strm;	//only will have effect when parameters are used
	    CleanupClosePushL(strm);
		//check if there is any parameters that need to be bound with the statement
	    TInt paramCnt = aOptParamFile.Count();
		if (paramCnt)
			{
			for (TInt paramId = 0; paramId<paramCnt; paramId++)
				{
				HBufC* paramFile = aOptParamFile[paramId];
				RFile file;
				CleanupClosePushL(file);
				err = file.Open(FsL(), *paramFile, EFileRead);
				LeaveIfErr(err, _L("Error openning file %S"), paramFile);
				
				err = strm.BindBinary(sqlState, paramId);
				User::LeaveIfError(err);
				
				//dump the file content the the parameter
				//file could be very large, so it is wise to separate it into several times. each time 32K or so.
				{
				TInt fileSize; 
				file.Size(fileSize);
				RBuf8 buf;
				buf.CreateL(KFileBufferSize);	//each time 32KB

				for (TInt curPos = 0; curPos<fileSize ;)
					{
					TInt remaingBytes = (fileSize-curPos);
					TInt bytesToProc = (remaingBytes>KFileBufferSize)? KFileBufferSize : remaingBytes;
					err = file.Read(curPos, buf, bytesToProc);
					User::LeaveIfError(err);
					
					strm.WriteL(buf);
					curPos += bytesToProc;
					}
				
				buf.Close();
				strm.CommitL();
				strm.Close();
				}
				
				CleanupStack::PopAndDestroy();	//file
				}
		
			}				
		err = sqlState.Exec();
		ReportResult(aCommand->Id(), aCommand->Name(), _L("Exec return code:%d\r\n"),err);
		
		CleanupStack::PopAndDestroy();	//strm
		}
	
	CleanupStack::PopAndDestroy();
	}


//close SQL database file
void CCmdSqlSrv::SqlClose(CServerCommandBase* aCommand)
	{
	if (!iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("No database opened"));
	
	iSqlDb.Close();
	//print the return code if it's not error
	ReportResult(aCommand->Id(), aCommand->Name(), _L("SQL database file closed\r\n"));
	iDatabaseOpened = EFalse;
	}

//attach additional database onto main database
void CCmdSqlSrv::SqlAttachL(TDesC& aFilename, TDesC& aDateBaseName, CServerCommandBase* aCommand)
	{
	TInt err;
	if (!iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("No database opened"));
	
	err = iSqlDb.Attach(aFilename, aDateBaseName);
	ReportResult(aCommand->Id(), aCommand->Name(), _L("RSqlDatabase::Attach return code:%d\r\n"), err);		
	User::LeaveIfError(err);	
	}

//
void CCmdSqlSrv::SqlDetachL(TDesC& aDateBaseName, CServerCommandBase* aCommand)
	{
	TInt err;
	if (!iDatabaseOpened)
		LeaveIfErr(KErrGeneral, _L("No database opened"));
	
	err = iSqlDb.Detach(aDateBaseName);
	ReportResult(aCommand->Id(), aCommand->Name(), _L("RSqlDatabase::Detach return code:%d\r\n"), err);		
	User::LeaveIfError(err);	
	}

#ifdef SQL_COMPACT
//RSqlDatabase::Compact() is documented but not implemented, so Do not use for now
//aDateBaseName: can be NULL (compacting main database)
void CCmdSqlSrv::SqlCompactL(TDesC* aDateBaseName, CServerCommandBase* aCommand)
	{
	TInt err;
	err = iSqlDb.Compact(0, aDateBaseName);
	ReportResult(aCommand->Id(), aCommand->Name(), _L("RSqlDatabase::Compact return code:%d\r\n"), err);		
	User::LeaveIfError(err);	
	}
#endif

////////////////////////////////////////////////////////////////////////////////////////////


//once we got a result from a SELECT command, then we go through the returned result
//only call this function after calling Next().
//we only analyse ONE returned record in this function
void CCmdSqlSrv::ParseResultL(RSqlStatement& aState, CServerCommandBase* aCommand, TDesC* aTempFileTemplate)
	{
	TInt err;
	TInt colCnt = aState.ColumnCount();
	TBuf<64> colHdr;	//column information fixed header
	
	for (TInt i=0; i<colCnt; i++)
		{
		TPtrC columnName;
		err = aState.ColumnName(i, columnName);
		if (err != KErrNone)
			{
			_LIT(KNotAvailable, "N/A");
			columnName.Set(KNotAvailable);
			}
				
		TSqlColumnType colType = aState.ColumnType(i);
		const TDesC* pTypeStr = ColumnTypeToString(colType);
		TInt colSize = aState.ColumnSize(i);
		
		colHdr.Format(_L("Column %d \"%S\"(%S)"), i, &columnName, pTypeStr);
		
		switch(colType)
			{
			case ESqlNull:
				{
				ReportResult(aCommand->Id(), aCommand->Name(), _L("%S \r\n"), &colHdr);	
				break;
				}
			case ESqlInt:
				{
				TInt colValue = aState.ColumnInt(i);
				ReportResult(aCommand->Id(), aCommand->Name(), _L("%S %d\r\n"), &colHdr, colValue);	
				break;
				}
			case ESqlInt64:
				{
				TInt64 colValue = aState.ColumnInt64(i);
				ReportResult(aCommand->Id(), aCommand->Name(),_L("%S %Ld\r\n"), &colHdr , colValue);
				break;
				}			
			case ESqlReal:
				{
				TInt64 colValue = aState.ColumnReal(i);
				ReportResult(aCommand->Id(), aCommand->Name(),_L("%S %f\r\n"), &colHdr, colValue);				
				break;
				}						
			case ESqlText:
				{
				TPtrC  colValue = aState.ColumnTextL(i);
				ReportResult(aCommand->Id(), aCommand->Name(),_L("%S %S\r\n"), &colHdr, &colValue);				
				break;
				}						
			case ESqlBinary:
				{
				TBool dumpToFile = EFalse;
				TBuf<128> printableHex;
				TFileName tmpFileName;
				
				RSqlColumnReadStream strm;
				CleanupClosePushL(strm);
				err = strm.ColumnBinary(aState, i);
				
				//generate a temp file name for dumping binary content
				RFile tmpFile;
				CleanupClosePushL(tmpFile);
				if (aTempFileTemplate && aTempFileTemplate->Length()>0 )
					{
					MakeTempFilename(tmpFileName, *aTempFileTemplate);
					//open the file
					err = tmpFile.Replace(FsL(), tmpFileName, EFileWrite);
					if (err==KErrNone)
						dumpToFile = ETrue;
					}
				
				//go through the binary content, and dump it if tmp file is available
				RBuf8 readBuf;
				CleanupClosePushL(readBuf);
				readBuf.CreateL(KFileBufferSize);
				for(TInt cursor=0; cursor < colSize; )
					{
					TInt bytesRemain = (colSize-cursor);
					TInt bytesToProc = (bytesRemain>KFileBufferSize) ? KFileBufferSize : bytesRemain;
					strm.ReadL(readBuf, bytesToProc);
					
					if (cursor==0)	//it is beginning the binary data, always print first few bytes on screen
						{
						TInt byteCnt = bytesToProc;
						TInt byteToPrint = (byteCnt > 32)? 32 : byteCnt;
						printableHex.AppendFormat(_L("DataLength:%d Hex: "), colSize);
						
						for (TInt a=0; a<byteToPrint; a++)
							{
							TInt byteVal = readBuf[a]; 
							printableHex.AppendFormat(_L("%02x "), byteVal);
							}
						if (byteToPrint < byteCnt)
							printableHex.AppendFormat(_L("..."));
						}
					
					if (dumpToFile)
						{
						err = tmpFile.Write(cursor, readBuf, bytesToProc);
						if (err != KErrNone) 
							{
							dumpToFile = EFalse;
							break;
							}
						}
					else
						break;
					
					cursor += bytesToProc; 
					}								
				CleanupStack::PopAndDestroy(); //readBuf			
				CleanupStack::PopAndDestroy(); //tmpFile
				CleanupStack::PopAndDestroy();//strm;				
				ReportResult(aCommand->Id(), aCommand->Name(),_L("%S %S\r\n"), &colHdr, &printableHex);
				if (dumpToFile)
					{
					ReportResult(aCommand->Id(), aCommand->Name(),_L("Binary content dumped to file %S\r\n"), 
							&tmpFileName);
					}
				}
				break;
			}
		
		}
	}



EXE_BOILER_PLATE(CCmdSqlSrv)

#define CASE_LIT(x) case x: { _LIT(KName, #x); pString = &KName; break; }

//static
const TDesC* CCmdSqlSrv::ColumnTypeToString(TSqlColumnType aType)
	{
	const TDesC* pString = NULL;
	switch(aType)
		{
		CASE_LIT(ESqlNull);
		CASE_LIT(ESqlInt);
		CASE_LIT(ESqlInt64);
		CASE_LIT(ESqlReal);
		CASE_LIT(ESqlText);
		CASE_LIT(ESqlBinary);
		default:
			_LIT(KUnknowStr, "Unknown");
			pString = &KUnknowStr;		
		}
	return pString;	
	}

void CCmdSqlSrv::PrintTime(const TTime& aTime, TBool aNewline)
	{	
	TTime NullTime = Time::NullTTime();
	if (aTime == NullTime) 
		{
		Printf(_L("(NullTime)"));
		}
	else
		{
		_LIT8(KDateTimeFormat, "%d-%02d-%02d %02d:%02d:%02d");
		TDateTime dt = aTime.DateTime();
		Printf(KDateTimeFormat, dt.Year(), dt.Month()+1, dt.Day()+1, dt.Hour(), dt.Minute(), dt.Second());
		}
	
	if (aNewline) Printf(_L("\r\n"));
	}
