using System;
using System.Linq;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Scoping;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Services.Implement
{
internal class KeyValueService : IKeyValueService
{
private readonly object _initialock = new object();
private readonly IScopeProvider _scopeProvider;
private readonly ILogger _logger;
private bool _initialized;
public KeyValueService(IScopeProvider scopeProvider, ILogger logger)
{
_scopeProvider = scopeProvider;
_logger = logger;
}
private void EnsureInitialized()
{
lock (_initialock)
{
if (_initialized) return;
Initialize();
_initialized = true;
}
}
private void Initialize()
{
// the key/value service is entirely self-managed, because it is used by the
// upgrader and anything we might change need to happen before everything else
// if already running 8, either following an upgrade or an install,
// then everything should be ok (the table should exist, etc)
if (UmbracoVersion.Local.Major >= 8)
return;
// else we are upgrading from 7, we can assume that the locks table
// exists, but we need to create everything for key/value
using (var scope = _scopeProvider.CreateScope())
{
var context = new MigrationContext(scope.Database, _logger);
var initMigration = new InitializeMigration(context);
initMigration.Migrate();
scope.Complete();
}
}
///
/// A custom migration that executes standalone during the Initialize phase of this service.
///
internal class InitializeMigration : MigrationBase
{
public InitializeMigration(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
// as long as we are still running 7 this migration will be invoked,
// but due to multiple restarts during upgrades, maybe the table
// exists already
if (TableExists(Constants.DatabaseSchema.Tables.KeyValue))
return;
Logger.Info("Creating KeyValue structure.");
// the locks table was initially created with an identity (auto-increment) primary key,
// but we don't want this, especially as we are about to insert a new row into the table,
// so here we drop that identity
DropLockTableIdentity();
// insert the lock object for key/value
Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new {id = Constants.Locks.KeyValues, name = "KeyValues", value = 1}).Do();
// create the key-value table
Create.Table().Do();
}
private void DropLockTableIdentity()
{
// one cannot simply drop an identity, that requires a bit of work
// create a temp. id column and copy values
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do();
Execute.Sql("update umbracoLock set nid = id").Do();
// drop the id column entirely (cannot just drop identity)
Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
// recreate the id column without identity and copy values
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do();
Execute.Sql("update umbracoLock set id = nid").Do();
// drop the temp. id column
Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do();
// complete the primary key
Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do();
}
}
///
public string GetValue(string key)
{
EnsureInitialized();
using (var scope = _scopeProvider.CreateScope())
{
var sql = scope.SqlContext.Sql().Select().From().Where(x => x.Key == key);
var dto = scope.Database.Fetch(sql).FirstOrDefault();
scope.Complete();
return dto?.Value;
}
}
///
public void SetValue(string key, string value)
{
EnsureInitialized();
using (var scope = _scopeProvider.CreateScope())
{
scope.WriteLock(Constants.Locks.KeyValues);
var sql = scope.SqlContext.Sql().Select().From().Where(x => x.Key == key);
var dto = scope.Database.Fetch(sql).FirstOrDefault();
if (dto == null)
{
dto = new KeyValueDto
{
Key = key,
Value = value,
Updated = DateTime.Now
};
scope.Database.Insert(dto);
}
else
{
dto.Value = value;
dto.Updated = DateTime.Now;
scope.Database.Update(dto);
}
scope.Complete();
}
}
///
public void SetValue(string key, string originValue, string newValue)
{
if (!TrySetValue(key, originValue, newValue))
throw new InvalidOperationException("Could not set the value.");
}
///
public bool TrySetValue(string key, string originValue, string newValue)
{
EnsureInitialized();
using (var scope = _scopeProvider.CreateScope())
{
scope.WriteLock(Constants.Locks.KeyValues);
var sql = scope.SqlContext.Sql().Select().From().Where(x => x.Key == key);
var dto = scope.Database.Fetch(sql).FirstOrDefault();
if (dto == null || dto.Value != originValue)
return false;
dto.Value = newValue;
dto.Updated = DateTime.Now;
scope.Database.Update(dto);
scope.Complete();
}
return true;
}
///
/// Gets a value directly from the database, no scope, nothing.
///
/// Used by to determine the runtime state.
internal static string GetValue(IUmbracoDatabase database, string key)
{
// not 8 yet = no key/value table, no value
if (UmbracoVersion.Local.Major < 8)
return null;
var sql = database.SqlContext.Sql()
.Select()
.From()
.Where(x => x.Key == key);
return database.FirstOrDefault(sql)?.Value;
}
}
}