//
// Copyright 2014-2015 Amazon.com,
// Inc. or its affiliates. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
using Amazon.Runtime.Internal.Util;
using System.Data.SQLite;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.IO;
using System.Globalization;
using System.Security;
using System.Security.Permissions;
using Amazon.Util.Internal;
namespace Amazon.CognitoSync.SyncManager.Internal
{
public partial class SQLiteLocalStorage : ILocalStorage
{
//datetime is converted to ticks and stored as string
private SQLiteConnection connection;
#region dispose methods
///
/// Releases the resources consumed by this object if disposing is true.
///
[SecuritySafeCritical]
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
connection.Close();
connection.Dispose();
}
}
#endregion
#region helper methods
[SecuritySafeCritical]
private void SetupDatabase()
{
//check if database already exists
var filePath = InternalSDKUtils.DetermineAppLocalStoragePath(DB_FILE_NAME);
var directoryPath = InternalSDKUtils.DetermineAppLocalStoragePath();
if (!Directory.Exists(directoryPath))
{
DirectoryInfo di = Directory.CreateDirectory(directoryPath);
}
if (!File.Exists(filePath))
SQLiteConnection.CreateFile(filePath);
connection = new SQLiteConnection(string.Format(CultureInfo.InvariantCulture, "Data Source={0};Version=3;", filePath));
connection.Open();
string createDatasetTable = "CREATE TABLE IF NOT EXISTS " + TABLE_DATASETS + "("
+ DatasetColumns.IDENTITY_ID + " TEXT NOT NULL,"
+ DatasetColumns.DATASET_NAME + " TEXT NOT NULL,"
+ DatasetColumns.CREATION_TIMESTAMP + " TEXT DEFAULT '0',"
+ DatasetColumns.LAST_MODIFIED_TIMESTAMP + " TEXT DEFAULT '0',"
+ DatasetColumns.LAST_MODIFIED_BY + " TEXT,"
+ DatasetColumns.STORAGE_SIZE_BYTES + " INTEGER DEFAULT 0,"
+ DatasetColumns.RECORD_COUNT + " INTEGER DEFAULT 0,"
+ DatasetColumns.LAST_SYNC_COUNT + " INTEGER NOT NULL DEFAULT 0,"
+ DatasetColumns.LAST_SYNC_TIMESTAMP + " TEXT DEFAULT '0',"
+ DatasetColumns.LAST_SYNC_RESULT + " TEXT,"
+ "UNIQUE (" + DatasetColumns.IDENTITY_ID + ", "
+ DatasetColumns.DATASET_NAME + ")"
+ ")";
using (var command = new SQLiteCommand(createDatasetTable, connection))
{
command.ExecuteNonQuery();
}
string createRecordsTable = "CREATE TABLE IF NOT EXISTS " + TABLE_RECORDS + "("
+ RecordColumns.IDENTITY_ID + " TEXT NOT NULL,"
+ RecordColumns.DATASET_NAME + " TEXT NOT NULL,"
+ RecordColumns.KEY + " TEXT NOT NULL,"
+ RecordColumns.VALUE + " TEXT,"
+ RecordColumns.SYNC_COUNT + " INTEGER NOT NULL DEFAULT 0,"
+ RecordColumns.LAST_MODIFIED_TIMESTAMP + " TEXT DEFAULT '0',"
+ RecordColumns.LAST_MODIFIED_BY + " TEXT,"
+ RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP + " TEXT DEFAULT '0',"
+ RecordColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 1,"
+ "UNIQUE (" + RecordColumns.IDENTITY_ID + ", " + RecordColumns.DATASET_NAME
+ ", " + RecordColumns.KEY + ")"
+ ")";
using (var command = new SQLiteCommand(createRecordsTable, connection))
{
command.ExecuteNonQuery();
}
string createKvStore = "CREATE TABLE IF NOT EXISTS kvstore (key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE (KEY))";
using (var command = new SQLiteCommand(createKvStore, connection))
{
command.ExecuteNonQuery();
}
}
[SecuritySafeCritical]
internal void CreateDatasetHelper(string query, params object[] parameters)
{
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, parameters);
command.ExecuteNonQuery();
}
}
[SecuritySafeCritical]
internal DatasetMetadata GetMetadataHelper(string identityId, string datasetName)
{
string query = DatasetColumns.BuildQuery(
DatasetColumns.IDENTITY_ID + " = @identityId AND " +
DatasetColumns.DATASET_NAME + " = @datasetName "
);
DatasetMetadata metadata = null;
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, identityId, datasetName);
using (var reader = command.ExecuteReader())
{
if (reader.HasRows && reader.Read())
{
metadata = SqliteStmtToDatasetMetadata(reader);
}
}
}
return metadata;
}
[SecuritySafeCritical]
internal List GetDatasetMetadataHelper(string query, params string[] parameters)
{
List datasetMetadataList = new List();
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, parameters);
using (var reader = command.ExecuteReader())
{
while (reader.HasRows && reader.Read())
{
datasetMetadataList.Add(SqliteStmtToDatasetMetadata(reader));
}
}
}
return datasetMetadataList;
}
[SecuritySafeCritical]
internal Record GetRecordHelper(string query, params string[] parameters)
{
Record record = null;
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, parameters);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
record = SqliteStmtToRecord(reader);
}
}
}
return record;
}
[SecuritySafeCritical]
internal List GetRecordsHelper(string query, params string[] parameters)
{
List records = new List();
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, parameters);
using (var reader = command.ExecuteReader())
{
while (reader.HasRows && reader.Read())
{
records.Add(SqliteStmtToRecord(reader));
}
}
}
return records;
}
[SecuritySafeCritical]
internal long GetLastSyncCountHelper(string query, params string[] parameters)
{
long lastSyncCount = 0;
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, parameters);
using (var reader = command.ExecuteReader())
{
if (reader.HasRows && reader.Read())
{
var nvc = reader.GetValues();
lastSyncCount = long.Parse(nvc[DatasetColumns.LAST_SYNC_COUNT], CultureInfo.InvariantCulture);
}
}
}
return lastSyncCount;
}
[SecuritySafeCritical]
internal List GetModifiedRecordsHelper(string query, params object[] parameters)
{
List records = new List();
using (var command = new SQLiteCommand(connection))
{
command.CommandText = query;
BindData(command, parameters);
using (var reader = command.ExecuteReader())
{
while (reader.HasRows && reader.Read())
{
records.Add(SqliteStmtToRecord(reader));
}
}
}
return records;
}
[SecuritySafeCritical]
internal void ExecuteMultipleHelper(List statements)
{
using (var transaction = connection.BeginTransaction())
{
foreach (var stmt in statements)
{
using (var command = connection.CreateCommand())
{
command.CommandText = stmt.Query;
command.Transaction = transaction;
BindData(command, stmt.Parameters);
command.ExecuteNonQuery();
}
}
transaction.Commit();
}
}
[SecuritySafeCritical]
internal void UpdateLastSyncCountHelper(string query, params object[] parameters)
{
using (var command = connection.CreateCommand())
{
command.CommandText = query;
BindData(command, parameters);
command.ExecuteNonQuery();
}
}
[SecuritySafeCritical]
internal void UpdateLastModifiedTimestampHelper(string query, params object[] parameters)
{
using (var command = connection.CreateCommand())
{
command.CommandText = query;
BindData(command, parameters);
command.ExecuteNonQuery();
}
}
[SecuritySafeCritical]
internal void UpdateOrInsertRecord(string identityId, string datasetName, Record record)
{
lock (sqlite_lock)
{
string checkRecordExistsQuery = "SELECT COUNT(*) FROM " + SQLiteLocalStorage.TABLE_RECORDS + " WHERE " +
RecordColumns.IDENTITY_ID + " = @whereIdentityId AND " +
RecordColumns.DATASET_NAME + " = @whereDatasetName AND " +
RecordColumns.KEY + " = @whereKey ";
bool recordsFound = false;
using (var command = connection.CreateCommand())
{
command.CommandText = checkRecordExistsQuery;
BindData(command, identityId, datasetName, record.Key);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
recordsFound = reader.GetInt32(0) > 0;
}
}
if (recordsFound)
{
string updateRecordQuery =
RecordColumns.BuildUpdate(
new string[] {
RecordColumns.VALUE,
RecordColumns.SYNC_COUNT,
RecordColumns.MODIFIED,
RecordColumns.LAST_MODIFIED_TIMESTAMP,
RecordColumns.LAST_MODIFIED_BY,
RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP
},
RecordColumns.IDENTITY_ID + " = @whereIdentityId AND " +
RecordColumns.DATASET_NAME + " = @whereDatasetName AND " +
RecordColumns.KEY + " = @whereKey "
);
using (var command = connection.CreateCommand())
{
command.CommandText = updateRecordQuery;
BindData(command, record.Value, record.SyncCount, record.IsModified ? 1 : 0, record.LastModifiedDate, record.LastModifiedBy, record.DeviceLastModifiedDate, identityId, datasetName, record.Key);
command.ExecuteNonQuery();
}
}
else
{
string insertRecord = RecordColumns.BuildInsert();
using (var command = new SQLiteCommand(insertRecord, connection))
{
BindData(command, identityId, datasetName, record.Key, record.Value, record.SyncCount, record.LastModifiedDate, record.LastModifiedBy, record.DeviceLastModifiedDate, record.IsModified ? 1 : 0);
command.ExecuteNonQuery();
}
}
}
}
#endregion
#region private methods
[SecuritySafeCritical]
private static void BindData(SQLiteCommand command, params object[] parameters)
{
string query = command.CommandText;
int count = 0;
foreach (Match match in Regex.Matches(query, "(\\@\\w+) "))
{
var date = parameters[count] as DateTime?;
if (date.HasValue)
{
command.Parameters.Add(new SQLiteParameter(match.Groups[1].Value, date.Value.Ticks.ToString(CultureInfo.InvariantCulture.NumberFormat)));
}
else
{
command.Parameters.Add(new SQLiteParameter(match.Groups[1].Value, parameters[count]));
}
count++;
}
}
[SecuritySafeCritical]
private static DatasetMetadata SqliteStmtToDatasetMetadata(SQLiteDataReader reader)
{
var nvc = reader.GetValues();
return new DatasetMetadata(
nvc[DatasetColumns.DATASET_NAME],
new DateTime(long.Parse(nvc[DatasetColumns.CREATION_TIMESTAMP], CultureInfo.InvariantCulture.NumberFormat)),
new DateTime(long.Parse(nvc[DatasetColumns.LAST_MODIFIED_TIMESTAMP], CultureInfo.InvariantCulture.NumberFormat)),
nvc[DatasetColumns.LAST_MODIFIED_BY],
long.Parse(nvc[DatasetColumns.STORAGE_SIZE_BYTES], CultureInfo.InvariantCulture.NumberFormat),
long.Parse(nvc[DatasetColumns.RECORD_COUNT], CultureInfo.InvariantCulture.NumberFormat)
);
}
[SecuritySafeCritical]
private static Record SqliteStmtToRecord(SQLiteDataReader reader)
{
var nvc = reader.GetValues();
return new Record(nvc[RecordColumns.KEY], nvc[RecordColumns.VALUE],
int.Parse(nvc[RecordColumns.SYNC_COUNT], CultureInfo.InvariantCulture), new DateTime(long.Parse(nvc[RecordColumns.LAST_MODIFIED_TIMESTAMP], CultureInfo.InvariantCulture.NumberFormat), DateTimeKind.Utc),
nvc[RecordColumns.LAST_MODIFIED_BY], new DateTime(long.Parse(nvc[RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP], CultureInfo.InvariantCulture.NumberFormat), DateTimeKind.Utc),
int.Parse(nvc[RecordColumns.MODIFIED], CultureInfo.InvariantCulture) == 1);
}
#endregion
#region BCL Specific implementation for identityId caching
///
/// cache the identity
///
///
///
[SecuritySafeCritical]
public void CacheIdentity(string key, string identity)
{
string query = "INSERT INTO kvstore(key,value) values ( @key , @value )";
using (var command = connection.CreateCommand())
{
command.CommandText = query;
BindData(command, key, identity);
command.ExecuteNonQuery();
}
}
///
/// Get the cached identity id
///
///
///
[SecuritySafeCritical]
public string GetIdentity(string key)
{
string query = "SELECT value FROM kvstore WHERE key = @key ";
using (var command = connection.CreateCommand())
{
command.CommandText = query;
BindData(command, key);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
return reader.GetString(0);
}
}
return null;
}
///
/// Delete the cached identity id
///
///
[SecuritySafeCritical]
public void DeleteCachedIdentity(string key)
{
string query = "delete from kvstore where key = @key ";
using (var command = connection.CreateCommand())
{
command.CommandText = query;
BindData(command, key);
command.ExecuteNonQuery();
}
}
#endregion
}
}