using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using Pfz.Databasing.Extensions.DatabaseNameExtensions;
using Pfz.Databasing.Filtering;
namespace Pfz.Databasing.Managers
{
/// <summary>
/// Creates a "database" manager that works over files and directories instead of
/// a real database. Good only for small projects, as there is no indexing or
/// transaction support.
/// </summary>
public sealed class SerializerDatabaseManager:
IDatabaseManager
{
/// <summary>
/// Creates the SerializerDatabaseManager using the given path and the BinaryFormatter.
/// </summary>
public SerializerDatabaseManager(string basePath):
this(basePath, new BinaryFormatter())
{
}
/// <summary>
/// Creates the SerializerDatabaseManager using the given path and formatter.
/// </summary>
/// <param name="basePath">The directory that is considered the "root" for the databases.</param>
/// <param name="formatter">The formatter used to read and write the records/files.</param>
public SerializerDatabaseManager(string basePath, IFormatter formatter)
{
if (string.IsNullOrEmpty(basePath))
throw new ArgumentNullException("basePath");
if (formatter == null)
throw new ArgumentNullException("formatter");
if (basePath[basePath.Length-1] != '\\')
basePath += '\\';
fBasePath = Path.GetFullPath(basePath);
fFormatter = formatter;
}
private string fBasePath;
/// <summary>
/// Gets the base directory path used by this manager.
/// </summary>
public string BasePath
{
get
{
return fBasePath;
}
}
private IFormatter fFormatter;
/// <summary>
/// Gets the formatter used by this manager.
/// </summary>
public IFormatter Formatter
{
get
{
return fFormatter;
}
}
/// <summary>
/// Uses the base path + the given name as the "database".
/// </summary>
public IDatabaseConnection CreateConnection(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException("name");
return new SerializerDatabaseConnection(fFormatter, this.BasePath + name + '\\');
}
/// <summary>
/// Tries to load a record of type T with the given primary key values.
/// </summary>
public T TryLoadByPrimaryKey<T>(IDatabaseConnection connection, params object[] primaryKeyValues)
where
T: class, IRecord
{
RecordImplementer.GetImplementedRecordsAssemblyFor<RecordClassGenerator>(typeof(T).Assembly);
var realConnection = (SerializerDatabaseConnection)connection;
string name = i_GetFileName(typeof(T), realConnection.fPath, primaryKeyValues);
IRecord record;
if (p_GetTransactionRecord(connection, name, out record))
return (T)record;
FileStream stream = null;
try
{
stream = File.OpenRead(name.ToString());
}
catch
{
}
if (stream == null)
return null;
using(stream)
{
object obj = fFormatter.Deserialize(stream);
RecordBase recordBase = (RecordBase)obj;
recordBase.SetRecordMode(RecordMode.ReadOnly);
return (T)obj;
}
}
private static bool p_GetTransactionRecord(IDatabaseConnection connection, string name, out IRecord record)
{
if (!connection.HasActiveTransaction)
{
record = null;
return false;
}
if (SerializerDatabaseConnection.fChangedRecords.TryGetValue(name, out record))
{
if (record.GetRecordMode() == RecordMode.ReadOnly)
{
record = null;
return true;
}
RecordBase rb = (RecordBase)record.Clone();
rb.SetUntypedOldRecord(null);
rb.SetRecordMode(RecordMode.ReadOnly);
record = rb;
return true;
}
record = null;
return false;
}
internal static string i_GetFileName(Type recordType, string path, object[] primaryKeyValues)
{
StringBuilder name = new StringBuilder(path);
name.Append(recordType.GetDatabaseName());
foreach (var value in primaryKeyValues)
{
name.Append('\\');
name.Append(value);
}
return name.ToString();
}
/// <summary>
/// Creates a new record.
/// </summary>
public T Create<T>(IDatabaseConnection connection)
where
T: class, IRecord
{
return RecordImplementer.Create<RecordClassGenerator, T>();
}
/// <summary>
/// Applies the given record.
/// </summary>
public T Apply<T>(IDatabaseConnection connection, T record)
where
T: class, IRecord
{
var realConnection = (SerializerDatabaseConnection)connection;
object[] primaryKeyValues = record.GetPrimaryKeyValues();
string name = i_GetFileName(record.GetType(), realConnection.fPath, primaryKeyValues);
IRecord recordAlreadyInTransaction;
SerializerDatabaseConnection.fChangedRecords.TryGetValue(name, out recordAlreadyInTransaction);
var recordMode = record.GetRecordMode();
switch(recordMode)
{
case RecordMode.Insert:
if (recordAlreadyInTransaction != null)
if (recordAlreadyInTransaction.GetRecordMode() != RecordMode.ReadOnly)
throw new DatabaseException("There is already a record with the same primary key.");
goto _update;
case RecordMode.Update:
_update:
SerializerDatabaseConnection.fChangedRecords[name] = (IRecord)record.Clone();
return record;
case RecordMode.ReadOnly:
if (!record.GetMustDeleteOnApply())
throw new DatabaseException("The actual record is read-only and must not be applied.");
SerializerDatabaseConnection.fChangedRecords[name] = record;
return null;
default:
throw new DatabaseException("Unknown RecordMode " + recordMode + ".");
}
}
/// <summary>
/// Always returns true.
/// </summary>
public bool MustCloneBeforeApply
{
get
{
return true;
}
}
/// <summary>
/// Loads records using the given filter.
/// </summary>
public IFastEnumerator<T> FastLoadByFilterGroup<T>(IDatabaseConnection connection, FilterGroup filterGroup)
where
T: class, IRecord
{
RecordImplementer.GetImplementedRecordsAssemblyFor<RecordClassGenerator>(typeof(T).Assembly);
var realConnection = (SerializerDatabaseConnection)connection;
List<FileInfo> files = new List<FileInfo>();
var directoryInfo = new DirectoryInfo(realConnection.fPath + typeof(T).GetDatabaseName());
if (directoryInfo.Exists)
p_GetFiles(files, directoryInfo);
return new p_FastLoadByFilterGroup<T>(realConnection, files, filterGroup, fFormatter);
}
/// <summary>
/// Return the count of records that matches the given filter.
/// </summary>
public int CountRecordsByFilterGroup<T>(IDatabaseConnection connection, FilterGroup filterGroup)
where
T: class, IRecord
{
RecordImplementer.GetImplementedRecordsAssemblyFor<RecordClassGenerator>(typeof(T).Assembly);
int count = 0;
using(var enumerator = FastLoadByFilterGroup<T>(connection, filterGroup))
while(enumerator.GetNext() != null)
count++;
return count;
}
private void p_GetFiles(List<FileInfo> files, DirectoryInfo directoryInfo)
{
foreach(var subDirectory in directoryInfo.GetDirectories())
p_GetFiles(files, subDirectory);
var directoryFiles = directoryInfo.GetFiles();
files.AddRange(directoryFiles);
}
/// <summary>
/// Loads records using the AdvancedLoadParameters.
/// </summary>
public IFastEnumerator<object[]> AdvancedLoad(IDatabaseConnection connection, AdvancedLoadParameters parameters)
{
var realConnection = (SerializerDatabaseConnection)connection;
List<FileInfo> files = new List<FileInfo>();
var directoryInfo = new DirectoryInfo(realConnection.fPath + parameters.InitialRecordType.FullName);
if (directoryInfo.Exists)
p_GetFiles(files, directoryInfo);
return new p_AdvancedLoad(realConnection, files, parameters, fFormatter);
}
T IDatabaseManager.CreateUserImplementation<T>(T record)
{
throw new NotImplementedException();
}
IFastEnumerator<T> IDatabaseManager.FastLoadByPartialSql<T>(IDatabaseConnection connection, string sql, params object[] parameterValues)
{
throw new NotImplementedException("SerializerDatabaseManager does not support loading by SQL strings.");
}
private static bool p_Matches(FilterGroup group, object record)
{
if (group == null)
return true;
if (group.fFilters.Count == 0 && group.fSubGroups.Count == 0)
return true;
if (group.GroupMode == FilterGroupMode.And)
{
foreach(var filter in group.fFilters)
if (!p_Matches(filter, record))
return false;
foreach(var subGroup in group.fSubGroups)
if (!p_Matches(subGroup, record))
return false;
return true;
}
foreach(var filter in group.fFilters)
if (p_Matches(filter, record))
return true;
foreach(var subGroup in group.fSubGroups)
if (p_Matches(subGroup, record))
return true;
return false;
}
private static bool p_Matches(Filter filter, object record)
{
object value = filter.Path.GetValue(record, filter.fTypePath);
switch(filter.Operation)
{
case FilterOperation.DifferentThan:
return !object.Equals(value, filter.Value);
case FilterOperation.EqualTo:
return object.Equals(value, filter.Value);
case FilterOperation.GreaterOrEqual:
return p_GetComparableValue(value, filter.Value) >= 0;
case FilterOperation.GreaterThan:
return p_GetComparableValue(value, filter.Value) > 0;
case FilterOperation.LessOrEqual:
return p_GetComparableValue(value, filter.Value) <= 0;
case FilterOperation.LessThan:
return p_GetComparableValue(value, filter.Value) < 0;
case FilterOperation.In:
throw new DatabaseException("SerializerDatabaseManager does not support IN operator.");
case FilterOperation.NotIn:
throw new DatabaseException("SerializerDatabaseManager does not support NOTIN operator.");
case FilterOperation.NotLike:
throw new DatabaseException("SerializerDatabaseManager does not support NOTLIKE operator.");
case FilterOperation.Like:
throw new DatabaseException("SerializerDatabaseManager does not support LIKE operator.");
case FilterOperation.StringStartsWith:
if (value == null)
return false;
return value.ToString().StartsWith(filter.Value.ToString());
case FilterOperation.StringNotStartsWith:
if (value == null)
return false;
return !value.ToString().StartsWith(filter.Value.ToString());
case FilterOperation.StringEndsWith:
if (value == null)
return false;
return value.ToString().EndsWith(filter.Value.ToString());
case FilterOperation.StringNotEndsWith:
if (value == null)
return false;
return !value.ToString().EndsWith(filter.Value.ToString());
case FilterOperation.StringContains:
if (value == null)
return false;
return value.ToString().Contains(filter.Value.ToString());
case FilterOperation.StringNotContains:
if (value == null)
return false;
return !value.ToString().Contains(filter.Value.ToString());
default:
throw new DatabaseException("Unknown filterOperation " + filter.Operation + ".");
}
}
private static int p_GetComparableValue(object filterValue, object value)
{
IComparable comparable = (IComparable)filterValue;
return comparable.CompareTo(value);
}
private sealed class p_FastLoadByFilterGroup<T>:
IFastEnumerator<T>
where
T: class, IRecord
{
private SerializerDatabaseConnection fConnection;
private List<FileInfo> fFiles;
private FilterGroup fGroup;
private int fFileIndex;
private IFormatter fFormatter;
public p_FastLoadByFilterGroup(SerializerDatabaseConnection connection, List<FileInfo> files, FilterGroup group, IFormatter formatter)
{
fConnection = connection;
fFiles = files;
fGroup = group;
fFormatter = formatter;
}
public void Dispose()
{
}
public T GetNext()
{
while(true)
{
if (fFileIndex >= fFiles.Count)
return null;
var fileInfo = fFiles[fFileIndex];
fFileIndex++;
T result;
IRecord record;
if (p_GetTransactionRecord(fConnection, fileInfo.FullName, out record))
{
if (record == null)
continue;
result = (T)record;
}
else
{
object obj;
using(var stream = fileInfo.OpenRead())
obj = fFormatter.Deserialize(stream);
result = (T)obj;
RecordBase recordBase = (RecordBase)obj;
recordBase.SetRecordMode(RecordMode.ReadOnly);
}
if (fGroup == null)
return result;
if (p_Matches(fGroup, result))
return result;
}
}
object IFastEnumerator.GetNext()
{
return GetNext();
}
}
private sealed class p_AdvancedLoad:
IFastEnumerator<object[]>,
IEqualityComparer<object[]>
{
private SerializerDatabaseConnection fConnection;
private List<FileInfo> fFiles;
private AdvancedLoadParameters fParameters;
private int fFileIndex;
private IFormatter fFormatter;
private object[] fResult;
private HashSet<object[]> fHashSet;
public p_AdvancedLoad(SerializerDatabaseConnection connection, List<FileInfo> files, AdvancedLoadParameters parameters, IFormatter formatter)
{
fConnection = connection;
fFiles = files;
fParameters = parameters;
fFormatter = formatter;
fResult = new object[parameters.SelectPaths.Count];
if (parameters.Distinct)
fHashSet = new HashSet<object[]>(this);
}
public void Dispose()
{
}
public object[] GetNext()
{
while(true)
{
if (fFileIndex >= fFiles.Count)
return null;
var fileInfo = fFiles[fFileIndex];
fFileIndex++;
object result;
IRecord record;
if (p_GetTransactionRecord(fConnection, fileInfo.Name, out record))
{
if (record == null)
continue;
result = record;
}
else
{
using(var stream = fileInfo.OpenRead())
result = fFormatter.Deserialize(stream);
}
if (p_Matches(fParameters.Filter, result))
{
var resultValues = p_GetValues(result);
if (fHashSet == null || fHashSet.Add(resultValues))
return resultValues;
}
}
}
private object[] p_GetValues(object result)
{
int itemIndex = -1;
foreach(var path in fParameters.SelectPaths)
{
itemIndex++;
Type[] typePaths = null;
if (fParameters.TypePaths != null)
typePaths = fParameters.TypePaths[itemIndex];
object value = path.GetValue(result, typePaths);
fResult[itemIndex] = value;
}
return fResult;
}
object IFastEnumerator.GetNext()
{
return GetNext();
}
public bool Equals(object[] x, object[] y)
{
return x.SequenceEqual(y);
}
public int GetHashCode(object[] values)
{
int result = 0;
foreach(var value in values)
if (value != null)
result ^= value.GetHashCode();
return result;
}
}
/// <summary>
/// Gets the implemented class type for the given record type.
/// </summary>
public Type GetImplementedTypeFor(Type type)
{
return RecordImplementer.GetImplementedTypeFor<RecordClassGenerator>(type);
}
/// <summary>
/// Gets the type of the generator used by this class.
/// Returns the typeof(RecordClassGenerator).
/// </summary>
public Type RecordClassGeneratorType
{
get
{
return typeof(RecordClassGenerator);
}
}
}
}