2017-12-07 16:45:25 +01:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2020-09-17 09:42:55 +02:00
using Microsoft.Extensions.Logging ;
2017-12-07 16:45:25 +01:00
using NPoco ;
using Umbraco.Core.Cache ;
2019-12-09 14:12:06 +01:00
using Umbraco.Core.Configuration ;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Models ;
2018-01-15 11:32:30 +01:00
using Umbraco.Core.Models.Entities ;
2017-12-28 09:06:33 +01:00
using Umbraco.Core.Persistence.Dtos ;
2017-12-07 16:45:25 +01:00
using Umbraco.Core.Persistence.Factories ;
using Umbraco.Core.Persistence.Querying ;
2017-12-12 15:04:13 +01:00
using Umbraco.Core.Scoping ;
2017-12-07 16:45:25 +01:00
namespace Umbraco.Core.Persistence.Repositories.Implement
{
/// <summary>
/// Represents a repository for doing CRUD operations for <see cref="Language"/>
/// </summary>
internal class LanguageRepository : NPocoRepositoryBase < int , ILanguage > , ILanguageRepository
{
2019-12-09 14:12:06 +01:00
private readonly IGlobalSettings _globalSettings ;
2018-04-21 09:57:28 +02:00
private readonly Dictionary < string , int > _codeIdMap = new Dictionary < string , int > ( StringComparer . OrdinalIgnoreCase ) ;
2018-04-11 15:31:21 +02:00
private readonly Dictionary < int , string > _idCodeMap = new Dictionary < int , string > ( ) ;
2020-09-17 09:42:55 +02:00
public LanguageRepository ( IScopeAccessor scopeAccessor , AppCaches cache , ILogger < LanguageRepository > logger , IGlobalSettings globalSettings )
2017-12-14 17:04:44 +01:00
: base ( scopeAccessor , cache , logger )
2019-12-09 14:12:06 +01:00
{
_globalSettings = globalSettings ;
}
2017-12-07 16:45:25 +01:00
2017-12-15 16:29:14 +01:00
protected override IRepositoryCachePolicy < ILanguage , int > CreateCachePolicy ( )
2017-12-07 16:45:25 +01:00
{
2018-04-12 22:53:04 +02:00
return new FullDataSetRepositoryCachePolicy < ILanguage , int > ( GlobalIsolatedCache , ScopeAccessor , GetEntityId , /*expires:*/ false ) ;
2017-12-07 16:45:25 +01:00
}
2018-10-03 19:03:22 +02:00
private FullDataSetRepositoryCachePolicy < ILanguage , int > TypedCachePolicy = > CachePolicy as FullDataSetRepositoryCachePolicy < ILanguage , int > ;
2018-04-12 22:53:04 +02:00
2017-12-07 16:45:25 +01:00
#region Overrides of RepositoryBase < int , Language >
protected override ILanguage PerformGet ( int id )
{
2020-09-04 01:30:47 +10:00
return PerformGetAll ( id ) . FirstOrDefault ( ) ;
2017-12-07 16:45:25 +01:00
}
protected override IEnumerable < ILanguage > PerformGetAll ( params int [ ] ids )
{
var sql = GetBaseQuery ( false ) . Where ( "umbracoLanguage.id > 0" ) ;
if ( ids . Any ( ) )
{
sql . Where ( "umbracoLanguage.id in (@ids)" , new { ids = ids } ) ;
}
//this needs to be sorted since that is the way legacy worked - default language is the first one!!
//even though legacy didn't sort, it should be by id
sql . OrderBy < LanguageDto > ( dto = > dto . Id ) ;
2018-04-11 15:31:21 +02:00
// get languages
2018-06-12 11:11:16 +02:00
var languages = Database . Fetch < LanguageDto > ( sql ) . Select ( ConvertFromDto ) . OrderBy ( x = > x . Id ) . ToList ( ) ;
2018-04-11 15:31:21 +02:00
// initialize the code-id map
lock ( _codeIdMap )
{
_codeIdMap . Clear ( ) ;
_idCodeMap . Clear ( ) ;
foreach ( var language in languages )
{
_codeIdMap [ language . IsoCode ] = language . Id ;
2018-04-21 09:57:28 +02:00
_idCodeMap [ language . Id ] = language . IsoCode . ToLowerInvariant ( ) ;
2018-04-11 15:31:21 +02:00
}
}
return languages ;
2017-12-07 16:45:25 +01:00
}
protected override IEnumerable < ILanguage > PerformGetByQuery ( IQuery < ILanguage > query )
{
var sqlClause = GetBaseQuery ( false ) ;
var translator = new SqlTranslator < ILanguage > ( sqlClause , query ) ;
var sql = translator . Translate ( ) ;
2018-07-05 16:00:53 +02:00
var dtos = Database . Fetch < LanguageDto > ( sql ) ;
2018-07-12 20:52:02 +01:00
return dtos . Select ( ConvertFromDto ) . ToList ( ) ;
2017-12-07 16:45:25 +01:00
}
#endregion
#region Overrides of NPocoRepositoryBase < int , Language >
protected override Sql < ISqlContext > GetBaseQuery ( bool isCount )
{
var sql = Sql ( ) ;
sql = isCount
? sql . SelectCount ( )
: sql . Select < LanguageDto > ( ) ;
sql
. From < LanguageDto > ( ) ;
return sql ;
}
protected override string GetBaseWhereClause ( )
{
return "umbracoLanguage.id = @id" ;
}
protected override IEnumerable < string > GetDeleteClauses ( )
{
var list = new List < string >
{
//NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?)
// but we still need to remove them
2019-09-22 11:10:02 +02:00
"DELETE FROM " + Constants . DatabaseSchema . Tables . DictionaryValue + " WHERE languageId = @id" ,
"DELETE FROM " + Constants . DatabaseSchema . Tables . PropertyData + " WHERE languageId = @id" ,
"DELETE FROM " + Constants . DatabaseSchema . Tables . ContentVersionCultureVariation + " WHERE languageId = @id" ,
"DELETE FROM " + Constants . DatabaseSchema . Tables . DocumentCultureVariation + " WHERE languageId = @id" ,
"DELETE FROM " + Constants . DatabaseSchema . Tables . TagRelationship + " WHERE tagId IN (SELECT id FROM " + Constants . DatabaseSchema . Tables . Tag + " WHERE languageId = @id)" ,
"DELETE FROM " + Constants . DatabaseSchema . Tables . Tag + " WHERE languageId = @id" ,
"DELETE FROM " + Constants . DatabaseSchema . Tables . Language + " WHERE id = @id"
2017-12-07 16:45:25 +01:00
} ;
return list ;
}
2018-07-18 12:27:14 +02:00
protected override Guid NodeObjectTypeId = > throw new NotImplementedException ( ) ;
2017-12-07 16:45:25 +01:00
#endregion
#region Unit of Work Implementation
protected override void PersistNewItem ( ILanguage entity )
{
2018-09-12 11:47:04 +02:00
// validate iso code and culture name
if ( entity . IsoCode . IsNullOrWhiteSpace ( ) | | entity . CultureName . IsNullOrWhiteSpace ( ) )
throw new InvalidOperationException ( "Cannot save a language without an ISO code and a culture name." ) ;
2018-03-30 01:20:40 +11:00
2019-06-28 09:19:11 +02:00
entity . AddingEntity ( ) ;
2017-12-07 16:45:25 +01:00
2018-09-12 11:47:04 +02:00
// deal with entity becoming the new default entity
2018-07-18 12:27:14 +02:00
if ( entity . IsDefault )
2018-03-22 23:15:52 +11:00
{
2018-09-12 11:47:04 +02:00
// set all other entities to non-default
// safe (no race cond) because the service locks languages
var setAllDefaultToFalse = Sql ( )
2018-09-13 14:59:45 +02:00
. Update < LanguageDto > ( u = > u . Set ( x = > x . IsDefault , false ) ) ;
2018-09-12 11:47:04 +02:00
Database . Execute ( setAllDefaultToFalse ) ;
2018-03-22 23:15:52 +11:00
}
2018-09-13 14:59:45 +02:00
// fallback cycles are detected at service level
2018-09-12 11:47:04 +02:00
// insert
2018-06-29 11:27:08 +01:00
var dto = LanguageFactory . BuildDto ( entity ) ;
2017-12-07 16:45:25 +01:00
var id = Convert . ToInt32 ( Database . Insert ( dto ) ) ;
entity . Id = id ;
entity . ResetDirtyProperties ( ) ;
}
protected override void PersistUpdatedItem ( ILanguage entity )
{
2018-09-12 11:47:04 +02:00
// validate iso code and culture name
if ( entity . IsoCode . IsNullOrWhiteSpace ( ) | | entity . CultureName . IsNullOrWhiteSpace ( ) )
throw new InvalidOperationException ( "Cannot save a language without an ISO code and a culture name." ) ;
2018-03-30 01:20:40 +11:00
2019-06-28 09:19:11 +02:00
entity . UpdatingEntity ( ) ;
2017-12-07 16:45:25 +01:00
2018-07-18 12:27:14 +02:00
if ( entity . IsDefault )
2018-03-22 23:15:52 +11:00
{
2018-09-12 11:47:04 +02:00
// deal with entity becoming the new default entity
// set all other entities to non-default
// safe (no race cond) because the service locks languages
var setAllDefaultToFalse = Sql ( )
2018-09-13 14:11:17 +02:00
. Update < LanguageDto > ( u = > u . Set ( x = > x . IsDefault , false ) ) ;
2018-09-12 11:47:04 +02:00
Database . Execute ( setAllDefaultToFalse ) ;
}
else
{
// deal with the entity not being default anymore
// which is illegal - another entity has to become default
var selectDefaultId = Sql ( )
. Select < LanguageDto > ( x = > x . Id )
. From < LanguageDto > ( )
2018-09-13 14:11:17 +02:00
. Where < LanguageDto > ( x = > x . IsDefault ) ;
2018-09-12 11:47:04 +02:00
var defaultId = Database . ExecuteScalar < int > ( selectDefaultId ) ;
if ( entity . Id = = defaultId )
throw new InvalidOperationException ( $"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead." ) ;
2018-03-22 23:15:52 +11:00
}
2017-12-07 16:45:25 +01:00
2019-10-10 20:45:16 +11:00
if ( entity . IsPropertyDirty ( nameof ( ILanguage . IsoCode ) ) )
{
//if the iso code is changing, ensure there's not another lang with the same code already assigned
var sameCode = Sql ( )
2019-11-05 13:45:42 +01:00
. SelectCount ( )
2019-10-10 20:45:16 +11:00
. From < LanguageDto > ( )
. Where < LanguageDto > ( x = > x . IsoCode = = entity . IsoCode & & x . Id ! = entity . Id ) ;
var countOfSameCode = Database . ExecuteScalar < int > ( sameCode ) ;
if ( countOfSameCode > 0 )
throw new InvalidOperationException ( $"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity." ) ;
}
2018-09-13 14:59:45 +02:00
// fallback cycles are detected at service level
2018-09-12 11:47:04 +02:00
// update
var dto = LanguageFactory . BuildDto ( entity ) ;
2017-12-07 16:45:25 +01:00
Database . Update ( dto ) ;
entity . ResetDirtyProperties ( ) ;
}
protected override void PersistDeletedItem ( ILanguage entity )
{
2018-09-12 11:47:04 +02:00
// validate that the entity is not the default language.
// safe (no race cond) because the service locks languages
2018-03-29 23:48:54 +11:00
2018-09-12 11:47:04 +02:00
var selectDefaultId = Sql ( )
. Select < LanguageDto > ( x = > x . Id )
. From < LanguageDto > ( )
2018-09-13 14:11:17 +02:00
. Where < LanguageDto > ( x = > x . IsDefault ) ;
2018-03-29 23:48:54 +11:00
2018-09-12 11:47:04 +02:00
var defaultId = Database . ExecuteScalar < int > ( selectDefaultId ) ;
if ( entity . Id = = defaultId )
throw new InvalidOperationException ( $"Cannot delete the default language ({entity.IsoCode})." ) ;
2017-12-07 16:45:25 +01:00
2018-07-21 08:24:08 +02:00
// We need to remove any references to the language if it's being used as a fall-back from other ones
2018-09-13 14:59:45 +02:00
var clearFallbackLanguage = Sql ( )
. Update < LanguageDto > ( u = > u
. Set ( x = > x . FallbackLanguageId , null ) )
. Where < LanguageDto > ( x = > x . FallbackLanguageId = = entity . Id ) ;
Database . Execute ( clearFallbackLanguage ) ;
2018-07-21 08:24:08 +02:00
2018-09-12 11:47:04 +02:00
// delete
base . PersistDeletedItem ( entity ) ;
2017-12-07 16:45:25 +01:00
}
#endregion
protected ILanguage ConvertFromDto ( LanguageDto dto )
{
2019-12-09 14:12:06 +01:00
var entity = LanguageFactory . BuildEntity ( _globalSettings , dto ) ;
2017-12-07 16:45:25 +01:00
return entity ;
}
2018-07-05 16:00:53 +02:00
2017-12-07 16:45:25 +01:00
public ILanguage GetByIsoCode ( string isoCode )
{
2018-10-03 19:03:22 +02:00
// ensure cache is populated, in a non-expensive way
if ( TypedCachePolicy ! = null )
TypedCachePolicy . GetAllCached ( PerformGetAll ) ;
2018-10-04 16:27:41 +02:00
2018-04-18 14:42:20 +02:00
var id = GetIdByIsoCode ( isoCode , throwOnNotFound : false ) ;
2018-04-21 09:57:28 +02:00
return id . HasValue ? Get ( id . Value ) : null ;
2018-04-11 15:31:21 +02:00
}
// fast way of getting an id for an isoCode - avoiding cloning
// _codeIdMap is rebuilt whenever PerformGetAll runs
2018-11-08 16:33:19 +01:00
public int? GetIdByIsoCode ( string isoCode , bool throwOnNotFound = true )
2018-04-11 15:31:21 +02:00
{
2018-04-21 09:57:28 +02:00
if ( isoCode = = null ) return null ;
2018-10-03 19:03:22 +02:00
// ensure cache is populated, in a non-expensive way
if ( TypedCachePolicy ! = null )
TypedCachePolicy . GetAllCached ( PerformGetAll ) ;
2018-10-04 16:27:41 +02:00
else
PerformGetAll ( ) ; //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap
2018-10-03 19:03:22 +02:00
2018-04-11 15:31:21 +02:00
lock ( _codeIdMap )
{
if ( _codeIdMap . TryGetValue ( isoCode , out var id ) ) return id ;
}
2018-04-18 14:42:20 +02:00
if ( throwOnNotFound )
throw new ArgumentException ( $"Code {isoCode} does not correspond to an existing language." , nameof ( isoCode ) ) ;
2018-04-11 15:31:21 +02:00
2018-11-05 13:59:55 +11:00
return null ;
}
2019-11-05 13:45:42 +01:00
2018-04-11 15:31:21 +02:00
// fast way of getting an isoCode for an id - avoiding cloning
// _idCodeMap is rebuilt whenever PerformGetAll runs
2018-11-05 13:59:55 +11:00
public string GetIsoCodeById ( int? id , bool throwOnNotFound = true )
2018-04-11 15:31:21 +02:00
{
2018-04-21 09:57:28 +02:00
if ( id = = null ) return null ;
2018-10-03 19:03:22 +02:00
// ensure cache is populated, in a non-expensive way
if ( TypedCachePolicy ! = null )
TypedCachePolicy . GetAllCached ( PerformGetAll ) ;
2018-10-04 16:27:41 +02:00
else
PerformGetAll ( ) ;
2018-10-03 19:03:22 +02:00
2018-04-11 15:31:21 +02:00
lock ( _codeIdMap ) // yes, we want to lock _codeIdMap
{
2018-04-21 09:57:28 +02:00
if ( _idCodeMap . TryGetValue ( id . Value , out var isoCode ) ) return isoCode ;
2018-04-11 15:31:21 +02:00
}
2018-04-18 14:42:20 +02:00
if ( throwOnNotFound )
throw new ArgumentException ( $"Id {id} does not correspond to an existing language." , nameof ( id ) ) ;
2018-11-05 13:59:55 +11:00
2018-04-18 14:42:20 +02:00
return null ;
2017-12-07 16:45:25 +01:00
}
2018-04-26 16:03:08 +02:00
public string GetDefaultIsoCode ( )
{
return GetDefault ( ) ? . IsoCode ;
}
public int? GetDefaultId ( )
{
return GetDefault ( ) ? . Id ;
}
// do NOT leak that language, it's not deep-cloned!
private ILanguage GetDefault ( )
{
2018-10-03 19:03:22 +02:00
// get all cached
var languages = ( TypedCachePolicy ? . GetAllCached ( PerformGetAll ) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests)
? ? CachePolicy . GetAll ( Array . Empty < int > ( ) , PerformGetAll ) ) . ToList ( ) ;
2018-09-13 14:11:17 +02:00
var language = languages . FirstOrDefault ( x = > x . IsDefault ) ;
2018-09-12 11:47:04 +02:00
if ( language ! = null ) return language ;
// this is an anomaly, the service/repo should ensure it cannot happen
2020-09-16 09:58:07 +02:00
Logger . LogWarning ( "There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language." ) ;
2018-09-12 11:47:04 +02:00
// still, don't kill the site, and return "something"
2018-04-26 16:03:08 +02:00
ILanguage first = null ;
2018-09-12 11:47:04 +02:00
foreach ( var l in languages )
2018-04-26 16:03:08 +02:00
{
2018-09-12 11:47:04 +02:00
if ( first = = null | | l . Id < first . Id )
first = l ;
2018-04-26 16:03:08 +02:00
}
return first ;
}
2017-12-07 16:45:25 +01:00
}
}