using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using LumiSoft.Net;
namespace LumiSoft.Data.lsDB
{
/// <summary>
/// Table what all columns are with fixed length.
/// </summary>
public class lsDB_FixedLengthTable : IDisposable
{
private LDB_DataColumnCollection m_pColumns = null;
private FileStream m_pDbFile = null;
private string m_DbFileName = "";
private bool m_TableLocked = false;
private long m_RowsStartOffset = -1;
private long m_ColumnsStartOffset = -1;
private int m_RowLength = -1;
private byte[] m_RowDataBuffer = null;
private lsDB_FixedLengthRecord m_pCurrentRecord = null;
private long m_FileLength = 0;
private long m_FilePosition = 0;
/// <summary>
/// Default constructor.
/// </summary>
public lsDB_FixedLengthTable()
{
m_pColumns = new LDB_DataColumnCollection(this);
}
#region method Dispose
/// <summary>
/// Clean up any resources being used.
/// </summary>
public void Dispose()
{
Close();
}
#endregion
#region method Open
/// <summary>
/// Opens specified data file.
/// </summary>
/// <param name="fileName">File name.</param>
public void Open(string fileName)
{
Open(fileName,0);
}
/// <summary>
/// Opens specified data file.
/// </summary>
/// <param name="fileName">File name.</param>
/// <param name="waitTime">If data base file is exclusively locked, then how many seconds to wait file to unlock before raising a error.</param>
public void Open(string fileName,int waitTime)
{
DateTime lockExpireTime = DateTime.Now.AddSeconds(waitTime);
while(true){
try{
m_pDbFile = File.Open(fileName,FileMode.Open,FileAccess.ReadWrite,FileShare.ReadWrite);
break;
}
catch(IOException x){
if(!File.Exists(fileName)){
throw new Exception("Specified database file '" + fileName + "' does not exists !");
}
// Make this because to get rid of "The variable 'x' is declared but never used"
string dummy = x.Message;
System.Threading.Thread.Sleep(15);
// Lock wait time timed out
if(DateTime.Now > lockExpireTime){
throw new Exception("Database file is locked and lock wait time expired !");
}
}
}
/* Table structure:
50 bytes - version
2 bytes - CRLF
4 bytes - Free rows count
2 bytes - CRLF
100 x 500 bytes - 100 columns info store
2 bytes - CRLF
... data pages
*/
m_DbFileName = fileName;
// TODO: check if LDB file
// Read version line (50 bytes + CRLF)
byte[] version = new byte[52];
ReadFromFile(0,version,0,version.Length);
// Read free rows count
byte[] freeRows = new byte[6];
ReadFromFile(0,freeRows,0,freeRows.Length);
long currentColumnOffset = 58;
// Read 100 column lines (500 + CRLF bytes each)
for(int i=0;i<100;i++){
byte[] columnInfo = new byte[102];
if(ReadFromFile(currentColumnOffset,columnInfo,0,columnInfo.Length) != columnInfo.Length){
throw new Exception("Invalid columns data area length !");
}
if(columnInfo[0] != '\0'){
m_pColumns.Parse(columnInfo);
}
currentColumnOffset += 102;
}
// Header terminator \0
m_pDbFile.Position++;
// No we have rows start offset
m_RowsStartOffset = m_pDbFile.Position;
// Store file length and position
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_pDbFile.Position;
// Calculate row length
m_RowLength = 1 + 2;
for(int i = 0;i < m_pColumns.Count;i++){
m_RowLength += m_pColumns[i].ColumnSize;
}
m_RowDataBuffer = new byte[m_RowLength];
}
#endregion
#region method Close
/// <summary>
/// Closes database file.
/// </summary>
public void Close()
{
if(m_pDbFile != null){
m_pDbFile.Close();
m_pDbFile = null;
m_DbFileName = "";
m_FileLength = 0;
m_FilePosition = 0;
m_RowLength = 0;
m_ColumnsStartOffset = 0;
m_RowsStartOffset = 0;
m_TableLocked = false;
m_RowDataBuffer = null;
m_pCurrentRecord = null;
}
}
#endregion
#region method Create
/// <summary>
/// Creates new database file.
/// </summary>
/// <param name="fileName">File name.</param>
public void Create(string fileName)
{
m_pDbFile = new FileStream(fileName,FileMode.CreateNew,FileAccess.ReadWrite,FileShare.None);
/* Table structure:
50 bytes - version
2 bytes - CRLF
4 bytes - Free rows count
2 bytes - CRLF
100 x 500 bytes - 100 columns info store
2 bytes - CRLF
... data pages
*/
// Version 50 + CRLF bytes
byte[] versionData = new byte[52];
versionData[0] = (byte)'1';
versionData[1] = (byte)'.';
versionData[2] = (byte)'0';
versionData[50] = (byte)'\r';
versionData[51] = (byte)'\n';
m_pDbFile.Write(versionData,0,versionData.Length);
// Free rows count
byte[] freeRows = new byte[6];
freeRows[4] = (byte)'\r';
freeRows[5] = (byte)'\n';
m_pDbFile.Write(freeRows,0,freeRows.Length);
m_ColumnsStartOffset = m_pDbFile.Position;
// 100 x 100 + CRLF bytes header lines
for(int i=0;i<100;i++){
byte[] data = new byte[100];
m_pDbFile.Write(data,0,data.Length);
m_pDbFile.Write(new byte[]{(int)'\r',(int)'\n'},0,2);
}
// Headers terminator char(0)
m_pDbFile.WriteByte((int)'\0');
// Rows start pointer
m_RowsStartOffset = m_pDbFile.Position - 1;
m_DbFileName = fileName;
// Store file length and position
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_pDbFile.Position;
// Calculate row length
m_RowLength = 1 + 2;
for(int i = 0;i < m_pColumns.Count;i++){
m_RowLength += m_pColumns[i].ColumnSize;
}
m_RowDataBuffer = new byte[m_RowLength];
}
#endregion
#region method LockTable
/// <summary>
/// Locks table.
/// </summary>
/// <param name="waitTime">If table is locked, then how many sconds to wait table to unlock, before teturning error.</param>
public void LockTable(int waitTime)
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
// Table is locked already, just skip locking
if(m_TableLocked){
return;
}
DateTime lockExpireTime = DateTime.Now.AddSeconds(waitTime);
while(true){
try{
// We just lock first byte
m_pDbFile.Lock(0,1);
m_TableLocked = true;
break;
}
// Catch the IOException generated if the
// specified part of the file is locked.
catch(IOException x){
// Make this because to get rid of "The variable 'x' is declared but never used"
string dummy = x.Message;
System.Threading.Thread.Sleep(15);
// Lock wait time timed out
if(DateTime.Now > lockExpireTime){
throw new Exception("Table is locked and lock wait time expired !");
}
}
}
}
#endregion
#region method UnlockTable
/// <summary>
/// Unlock table.
/// </summary>
public void UnlockTable()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
if(m_TableLocked){
// We just unlock first byte
m_pDbFile.Unlock(0,1);
}
}
#endregion
#region method MoveFirstRecord
/// <summary>
/// Moves to first record.
/// </summary>
public void MoveFirstRecord()
{
m_pCurrentRecord = null;
// NextRecord();
}
#endregion
#region method NextRecord
/// <summary>
/// Gets next record. Returns true if end of file reached and there are no more records.
/// </summary>
/// <returns>Returns true if end of file reached and there are no more records.</returns>
public bool NextRecord()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
//--- Find next record ---------------------------------------------------//
long nextRowStartOffset = 0;
if(m_pCurrentRecord == null){
nextRowStartOffset = m_RowsStartOffset;
}
else{
nextRowStartOffset = m_pCurrentRecord.Pointer + m_RowLength;
}
while(true){
if(m_FileLength > nextRowStartOffset){
ReadFromFile(nextRowStartOffset,m_RowDataBuffer,0,m_RowLength);
// We want used row
if(m_RowDataBuffer[0] == 'u'){
if(m_pCurrentRecord == null){
m_pCurrentRecord = new lsDB_FixedLengthRecord(this,nextRowStartOffset,m_RowDataBuffer);
}
else{
m_pCurrentRecord.ReuseRecord(this,nextRowStartOffset,m_RowDataBuffer);
}
break;
}
}
else{
return true;
}
nextRowStartOffset += m_RowLength;
}
//-------------------------------------------------------------------------//
return false;
}
#endregion
#region method AppendRecord
/// <summary>
/// Appends new record to table.
/// </summary>
public void AppendRecord(object[] values)
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
if(m_pColumns.Count != values.Length){
throw new Exception("Each column must have corresponding value !");
}
bool unlock = true;
// Table is already locked, don't lock it
if(this.TableLocked){
unlock = false;
}
else{
LockTable(15);
}
/* Fixed record structure:
1 byte - specified is row is used or free space
u - used
f - free space
x bytes - columns data
2 bytes - CRLF
*/
int rowLength = 1 + 2;
for(int i = 0;i < m_pColumns.Count;i++){
rowLength += m_pColumns[i].ColumnSize;
}
int position = 1;
byte[] record = new byte[rowLength];
record[0] = (int)'u';
record[rowLength - 2] = (int)'\r';
record[rowLength - 1] = (int)'\n';
for(int i = 0;i < values.Length;i++){
byte[] columnData = LDB_Record.ConvertToInternalData(m_pColumns[i],values[i]);
// Check that column won't exceed maximum length.
if(columnData.Length > m_pColumns[i].ColumnSize){
throw new Exception("Column '" + m_pColumns[i] + "' exceeds maximum value length !");
}
Array.Copy(columnData,0,record,position,columnData.Length);
position += columnData.Length;
}
// Find free row
byte[] freeRowsBuffer = new byte[4];
ReadFromFile(52,freeRowsBuffer,0,freeRowsBuffer.Length);
int freeRows = ldb_Utils.ByteToInt(freeRowsBuffer,0);
// There are plenty free rows, find first
if(freeRows > 100){
//--- Find free record ---------------------------------------------------//
long nextRowStartOffset = m_RowsStartOffset;
long rowOffset = 0;
byte[] rowData = new byte[m_RowLength];
while(true){
ReadFromFile(nextRowStartOffset,rowData,0,m_RowLength);
// We want used row
if(rowData[0] == 'f'){
rowOffset = nextRowStartOffset;
break;
}
nextRowStartOffset += m_RowLength;
}
//-------------------------------------------------------------------------//
// Write new record to file
WriteToFile(rowOffset,record,0,record.Length);
// Update free rows count
WriteToFile(52,ldb_Utils.IntToByte(freeRows - 1),0,4);
}
// There are few empty rows, just append it
else{
AppendToFile(record,0,record.Length);
}
if(unlock){
UnlockTable();
}
}
#endregion
#region method DeleteRecord
/// <summary>
/// Deletes current record.
/// </summary>
public void DeleteCurrentRecord()
{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
if(m_pCurrentRecord == null){
throw new Exception("There is no current record !");
}
bool unlock = true;
// Table is already locked, don't lock it
if(this.TableLocked){
unlock = false;
}
else{
LockTable(15);
}
byte[] data = new byte[m_RowLength];
data[0] = (byte)'f';
data[m_RowLength - 2] = (byte)'\r';
data[m_RowLength - 1] = (byte)'\n';
WriteToFile(m_pCurrentRecord.Pointer,data,0,data.Length);
// Update free rows count
byte[] freeRowsBuffer = new byte[4];
ReadFromFile(52,freeRowsBuffer,0,freeRowsBuffer.Length);
int freeRows = ldb_Utils.ByteToInt(freeRowsBuffer,0);
WriteToFile(52,ldb_Utils.IntToByte(freeRows + 1),0,4);
if(unlock){
UnlockTable();
}
// Activate next record **** Change it ???
NextRecord();
}
#endregion
#region method AddColumn
/// <summary>
/// Adds column to db file.
/// </summary>
/// <param name="column"></param>
internal void AddColumn(LDB_DataColumn column)
{
if(column.ColumnSize < 1){
throw new Exception("Invalid column size '" + column.ColumnSize + "' for column '" + column.ColumnName + "' !");
}
// Find free column data area
long currentColumnOffset = m_ColumnsStartOffset;
long freeColumnPosition = -1;
// Loop all columns data areas, see it there any free left
for(int i=0;i<100;i++){
byte[] columnInfo = new byte[102];
if(ReadFromFile(currentColumnOffset,columnInfo,0,columnInfo.Length) != columnInfo.Length){
throw new Exception("Invalid columns data area length !");
}
// We found unused column data area
if(columnInfo[0] == '\0'){
freeColumnPosition = currentColumnOffset - 102;
break;
}
currentColumnOffset += 102;
}
if(freeColumnPosition != -1){
// TODO: If there is data ???
// Store column
byte[] columnData = column.ToColumnInfo();
WriteToFile(currentColumnOffset,columnData,0,columnData.Length);
}
else{
throw new Exception("Couldn't find free column space ! ");
}
}
#endregion
#region method RemoveColumn
/// <summary>
/// Removes specified column from database file.
/// </summary>
/// <param name="column"></param>
internal void RemoveColumn(LDB_DataColumn column)
{
throw new Exception("TODO:");
}
#endregion
#region method ReadFromFile
/// <summary>
/// Reads data from file.
/// </summary>
/// <param name="readOffset">Offset in database file from where to start reading data.</param>
/// <param name="data">Buffer where to store readed data.</param>
/// <param name="offset">Offset in array to where to start storing readed data.</param>
/// <param name="count">Number of bytes to read.</param>
/// <returns></returns>
internal int ReadFromFile(long readOffset,byte[] data,int offset,int count)
{
SetFilePosition(readOffset);
int readed = m_pDbFile.Read(data,offset,count);
m_FilePosition += readed;
return readed;
}
#endregion
#region method WriteToFile
/// <summary>
/// Writes data to file.
/// </summary>
/// <param name="writeOffset">Offset in database file from where to start writing data.</param>
/// <param name="data">Data to write.</param>
/// <param name="offset">Offset in array from where to start writing data.</param>
/// <param name="count">Number of bytes to write.</param>
/// <returns></returns>
internal void WriteToFile(long writeOffset,byte[] data,int offset,int count)
{
SetFilePosition(writeOffset);
m_pDbFile.Write(data,offset,count);
m_FilePosition += count;
}
#endregion
#region method AppendToFile
/// <summary>
/// Appends specified data at the end of file.
/// </summary>
/// <param name="data">Data to write.</param>
/// <param name="offset">Offset in array from where to start writing data.</param>
/// <param name="count">Number of bytes to write.</param>
internal void AppendToFile(byte[] data,int offset,int count)
{
m_pDbFile.Position = m_pDbFile.Length;
m_pDbFile.Write(data,offset,count);
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_pDbFile.Position;
}
#endregion
#region method GetFilePosition
/// <summary>
/// Gets current position in file.
/// </summary>
/// <returns></returns>
internal long GetFilePosition()
{
return m_FilePosition;
}
#endregion
#region method SetFilePosition
/// <summary>
/// Sets file position.
/// </summary>
/// <param name="position">Position in file.</param>
private void SetFilePosition(long position)
{
if(m_FilePosition != position){
m_pDbFile.Position = position;
m_FilePosition = position;
}
}
#endregion
// REMOVE ME:
#region method GoToFileEnd
/*
/// <summary>
/// Moves position to the end of file.
/// </summary>
private void GoToFileEnd()
{
m_pDbFile.Position = m_pDbFile.Length;
m_FileLength = m_pDbFile.Length;
m_FilePosition = m_FileLength;
}
*/
#endregion
#region Properties Implementation
/// <summary>
/// Gets if there is active database file.
/// </summary>
public bool IsDatabaseOpen
{
get{ return m_pDbFile != null; }
}
/// <summary>
/// Gets open database file name. Throws exception if database isn't open.
/// </summary>
public string FileName
{
get{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
return m_DbFileName;
}
}
/// <summary>
/// Gets table columns. Throws exception if database isn't open.
/// </summary>
public LDB_DataColumnCollection Columns
{
get{
if(!this.IsDatabaseOpen){
throw new Exception("Database isn't open, please open database first !");
}
return m_pColumns;
}
}
/// <summary>
/// Gets current record. Returns null if there isn't current record.
/// </summary>
public lsDB_FixedLengthRecord CurrentRecord
{
get{ return m_pCurrentRecord; }
}
/// <summary>
/// Gets table is locked.
/// </summary>
public bool TableLocked
{
get{ return m_TableLocked; }
}
#endregion
}
}