Files
Umbraco-CMS/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs

337 lines
14 KiB
C#
Raw Normal View History

using System;
2017-12-07 16:45:25 +01:00
using System.Collections.Generic;
using System.Linq;
2020-09-17 09:42:55 +02:00
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
2017-12-07 16:45:25 +01:00
using NPoco;
Merge remote-tracking branch 'origin/v8/8.16' into v9/feature/merge_v8_11082021 # Conflicts: # .github/CONTRIBUTING.md # build/NuSpecs/UmbracoCms.Core.nuspec # build/NuSpecs/UmbracoCms.Web.nuspec # build/NuSpecs/UmbracoCms.nuspec # src/SolutionInfo.cs # src/Umbraco.Core/Cache/AppCaches.cs # src/Umbraco.Core/Cache/AppPolicedCacheDictionary.cs # src/Umbraco.Core/Cache/DeepCloneAppCache.cs # src/Umbraco.Core/Cache/WebCachingAppCache.cs # src/Umbraco.Core/CompositionExtensions.cs # src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs # src/Umbraco.Core/Models/PropertyGroupCollection.cs # src/Umbraco.Core/Models/PropertyTypeCollection.cs # src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs # src/Umbraco.Core/ReadLock.cs # src/Umbraco.Core/Routing/SiteDomainMapper.cs # src/Umbraco.Core/UpgradeableReadLock.cs # src/Umbraco.Core/WriteLock.cs # src/Umbraco.Examine/ExamineExtensions.cs # src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs # src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs # src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs # src/Umbraco.Infrastructure/Services/IdKeyMap.cs # src/Umbraco.Infrastructure/Services/Implement/ContentService.cs # src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs # src/Umbraco.Tests/App.config # src/Umbraco.Web.BackOffice/Controllers/EntityController.cs # src/Umbraco.Web.UI.Client/package.json # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco.Web.UI.csproj # src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml # src/Umbraco.Web.UI/web.Template.config # src/Umbraco.Web/CacheHelperExtensions.cs # src/Umbraco.Web/Editors/RelationTypeController.cs # src/Umbraco.Web/Logging/WebProfilerProvider.cs # src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs # src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs # src/Umbraco.Web/Routing/ContentFinderByConfigured404.cs # src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs # src/Umbraco.Web/Security/BackOfficeUserManager.cs # src/Umbraco.Web/Umbraco.Web.csproj
2021-08-11 19:11:35 +02:00
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
using Umbraco.Extensions;
2017-12-07 16:45:25 +01:00
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
2017-12-07 16:45:25 +01:00
{
/// <summary>
/// Represents a repository for doing CRUD operations for <see cref="Language"/>
/// </summary>
2020-12-22 10:30:16 +11:00
internal class LanguageRepository : EntityRepositoryBase<int, ILanguage>, ILanguageRepository
2017-12-07 16:45:25 +01:00
{
private readonly GlobalSettings _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-18 12:53:06 +02:00
public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger<LanguageRepository> logger, IOptions<GlobalSettings> globalSettings)
2017-12-14 17:04:44 +01:00
: base(scopeAccessor, cache, logger)
{
_globalSettings = globalSettings.Value;
}
2017-12-07 16:45:25 +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
}
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)
{
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<LanguageDto>(x => x.Id > 0);
2017-12-07 16:45:25 +01:00
if (ids.Any())
{
sql.WhereIn<LanguageDto>(x => x.Id, ids);
2017-12-07 16:45:25 +01:00
}
//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>(x => x.Id);
2017-12-07 16:45:25 +01:00
2018-04-11 15:31:21 +02:00
// get languages
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();
var dtos = Database.Fetch<LanguageDto>(sql);
return dtos.Select(ConvertFromDto).ToList();
2017-12-07 16:45:25 +01:00
}
#endregion
2020-12-22 10:30:16 +11:00
#region Overrides of EntityRepositoryBase<int,Language>
2017-12-07 16:45:25 +01:00
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
{
var sql = Sql();
sql = isCount
? sql.SelectCount()
: sql.Select<LanguageDto>();
sql.From<LanguageDto>();
2017-12-07 16:45:25 +01:00
return sql;
}
protected override string GetBaseWhereClause()
{
return $"{Constants.DatabaseSchema.Tables.Language}.id = @id";
2017-12-07 16:45:25 +01:00
}
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
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id",
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id",
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id",
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id",
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)",
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id",
"DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Language + " WHERE id = @id"
2017-12-07 16:45:25 +01:00
};
return list;
}
#endregion
#region Unit of Work Implementation
protected override void PersistNewItem(ILanguage entity)
{
// 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.");
entity.AddingEntity();
2017-12-07 16:45:25 +01:00
// deal with entity becoming the new default entity
2018-07-18 12:27:14 +02:00
if (entity.IsDefault)
{
// 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));
Database.Execute(setAllDefaultToFalse);
}
2018-09-13 14:59:45 +02:00
// fallback cycles are detected at service level
// 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)
{
// 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.");
entity.UpdatingEntity();
2017-12-07 16:45:25 +01:00
2018-07-18 12:27:14 +02:00
if (entity.IsDefault)
{
// 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()
.Update<LanguageDto>(u => u.Set(x => x.IsDefault, false));
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>()
.Where<LanguageDto>(x => x.IsDefault);
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.");
}
2017-12-07 16:45:25 +01: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()
.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
// 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)
{
// validate that the entity is not the default language.
// safe (no race cond) because the service locks languages
var selectDefaultId = Sql()
.Select<LanguageDto>(x => x.Id)
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsDefault);
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
// 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);
// delete
base.PersistDeletedItem(entity);
2017-12-07 16:45:25 +01:00
}
#endregion
protected ILanguage ConvertFromDto(LanguageDto dto)
{
var entity = LanguageFactory.BuildEntity(_globalSettings, dto);
2017-12-07 16:45:25 +01:00
return entity;
}
2017-12-07 16:45:25 +01:00
public ILanguage GetByIsoCode(string isoCode)
{
// ensure cache is populated, in a non-expensive way
if (TypedCachePolicy != null)
TypedCachePolicy.GetAllCached(PerformGetAll);
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;
// ensure cache is populated, in a non-expensive way
if (TypedCachePolicy != null)
TypedCachePolicy.GetAllCached(PerformGetAll);
else
PerformGetAll(); //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap
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
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
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;
// ensure cache is populated, in a non-expensive way
if (TypedCachePolicy != null)
TypedCachePolicy.GetAllCached(PerformGetAll);
else
PerformGetAll();
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-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()
{
// 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();
var language = languages.FirstOrDefault(x => x.IsDefault);
if (language != null) return language;
// this is an anomaly, the service/repo should ensure it cannot happen
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.");
// still, don't kill the site, and return "something"
2018-04-26 16:03:08 +02:00
ILanguage first = null;
foreach (var l in languages)
2018-04-26 16:03:08 +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
}
}