Files
Umbraco-CMS/src/Umbraco.Infrastructure/PublishedContentQuery.cs

405 lines
15 KiB
C#
Raw Normal View History

Examine 2.0 integration (#10241) * Init commit for examine 2.0 work, most old umb examine tests working, probably a lot that doesn't * Gets Umbraco Examine tests passing and makes some sense out of them, fixes some underlying issues. * Large refactor, remove TaskHelper, rename Notifications to be consistent, Gets all examine/lucene indexes building and startup ordered in the correct way, removes old files, creates new IUmbracoIndexingHandler for abstracting out all index operations for umbraco data, abstracts out IIndexRebuilder, Fixes Stack overflow with LiveModelsProvider and loading assemblies, ports some changes from v8 for startup handling with cold boots, refactors out LastSyncedFileManager * fix up issues with rebuilding and management dashboard. * removes old files, removes NetworkHelper, fixes LastSyncedFileManager implementation to ensure the machine name is used, fix up logging with cold boot state. * Makes MainDom safer to use and makes PublishedSnapshotService lazily register with MainDom * lazily acquire application id (fix unit tests) * Fixes resource casing and missing test file * Ensures caches when requiring internal services for PublishedSnapshotService, UseNuCache is a separate call, shouldn't be buried in AddWebComponents, was also causing issues in integration tests since nucache was being used for the Id2Key service. * For UmbracoTestServerTestBase enable nucache services * Fixing tests * Fix another test * Fixes tests, use TestHostingEnvironment, make Tests.Common use net5, remove old Lucene.Net.Contrib ref. * Fixes up some review notes * Fixes issue with doubly registering PublishedSnapshotService meanig there could be 2x instances of it * Checks for parseexception when executing the query * Use application root instead of duplicating functionality. * Added Examine project to netcore only solution file * Fixed casing issue with LazyLoad, that is not lowercase. * uses cancellationToken instead of bool flag, fixes always reading lastId from the LastSyncedFileManager, fixes RecurringHostedServiceBase so that there isn't an overlapping thread for the same task type * Fix tests * remove legacy test project from solution file * Fix test Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-05-18 18:31:38 +10:00
using System;
using System.Collections;
2018-06-29 19:52:40 +02:00
using System.Collections.Generic;
2021-09-15 13:40:08 +02:00
using System.Globalization;
2018-06-29 19:52:40 +02:00
using System.Linq;
using System.Xml.XPath;
using Examine;
using Examine.Search;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Xml;
2021-02-12 10:57:50 +01:00
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
2018-06-29 19:52:40 +02:00
namespace Umbraco.Cms.Infrastructure
2018-06-29 19:52:40 +02:00
{
/// <summary>
/// A class used to query for published content, media items
2018-06-29 19:52:40 +02:00
/// </summary>
/// <seealso cref="Umbraco.Cms.Core.IPublishedContentQuery" />
2018-06-29 19:52:40 +02:00
public class PublishedContentQuery : IPublishedContentQuery
{
private readonly IExamineManager _examineManager;
2019-02-07 09:59:16 +01:00
private readonly IPublishedSnapshot _publishedSnapshot;
private readonly IVariationContextAccessor _variationContextAccessor;
private static readonly HashSet<string> s_itemIdFieldNameHashSet =
new HashSet<string>() { ExamineFieldNames.ItemIdFieldName };
2018-06-29 19:52:40 +02:00
/// <summary>
/// Initializes a new instance of the <see cref="PublishedContentQuery" /> class.
2018-06-29 19:52:40 +02:00
/// </summary>
public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor, IExamineManager examineManager)
2018-06-29 19:52:40 +02:00
{
2019-02-07 09:59:16 +01:00
_publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot));
_variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor));
_examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager));
2018-06-29 19:52:40 +02:00
}
#region Convert Helpers
2018-06-29 19:52:40 +02:00
private static bool ConvertIdObjectToInt(object id, out int intId)
2018-06-29 19:52:40 +02:00
{
switch (id)
{
case string s:
2021-09-15 13:40:08 +02:00
return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out intId);
case int i:
intId = i;
return true;
default:
intId = default;
return false;
}
2018-06-29 19:52:40 +02:00
}
private static bool ConvertIdObjectToGuid(object id, out Guid guidId)
2018-06-29 19:52:40 +02:00
{
switch (id)
{
case string s:
return Guid.TryParse(s, out guidId);
case Guid g:
guidId = g;
return true;
default:
guidId = default;
return false;
}
}
2022-02-22 13:35:32 +01:00
private static bool ConvertIdObjectToUdi(object id, out Udi? guidId)
{
switch (id)
{
case string s:
return UdiParser.TryParse(s, out guidId);
case Udi u:
guidId = u;
return true;
default:
guidId = default;
return false;
}
2018-06-29 19:52:40 +02:00
}
#endregion
#region Content
2022-03-16 14:39:28 +01:00
public IPublishedContent? Content(int id)
=> ItemById(id, _publishedSnapshot.Content);
public IPublishedContent? Content(Guid id)
=> ItemById(id, _publishedSnapshot.Content);
2022-02-22 13:35:32 +01:00
public IPublishedContent? Content(Udi? id)
2018-06-29 19:52:40 +02:00
{
2022-03-16 14:39:28 +01:00
if (!(id is GuidUdi udi))
{
return null;
}
2019-02-07 09:59:16 +01:00
return ItemById(udi.Guid, _publishedSnapshot.Content);
2018-06-29 19:52:40 +02:00
}
2022-02-22 13:35:32 +01:00
public IPublishedContent? Content(object id)
2018-06-29 19:52:40 +02:00
{
if (ConvertIdObjectToInt(id, out var intId))
{
return Content(intId);
}
if (ConvertIdObjectToGuid(id, out var guidId))
{
return Content(guidId);
}
if (ConvertIdObjectToUdi(id, out var udiId))
{
return Content(udiId);
}
return null;
2018-06-29 19:52:40 +02:00
}
public IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars)
=> ItemByXPath(xpath, vars, _publishedSnapshot.Content);
2018-06-29 19:52:40 +02:00
public IEnumerable<IPublishedContent> Content(IEnumerable<int> ids)
=> ItemsByIds(_publishedSnapshot.Content, ids);
2018-06-29 19:52:40 +02:00
public IEnumerable<IPublishedContent> Content(IEnumerable<Guid> ids)
=> ItemsByIds(_publishedSnapshot.Content, ids);
2018-06-29 19:52:40 +02:00
public IEnumerable<IPublishedContent> Content(IEnumerable<object> ids)
=> ids.Select(Content).WhereNotNull();
public IEnumerable<IPublishedContent> ContentAtXPath(string xpath, params XPathVariable[] vars)
=> ItemsByXPath(xpath, vars, _publishedSnapshot.Content);
2018-06-29 19:52:40 +02:00
public IEnumerable<IPublishedContent> ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars)
=> ItemsByXPath(xpath, vars, _publishedSnapshot.Content);
public IEnumerable<IPublishedContent> ContentAtRoot()
=> ItemsAtRoot(_publishedSnapshot.Content);
2018-06-29 19:52:40 +02:00
#endregion
#region Media
2022-03-16 14:39:28 +01:00
public IPublishedContent? Media(int id)
=> ItemById(id, _publishedSnapshot.Media);
2018-06-29 19:52:40 +02:00
public IPublishedContent? Media(Guid id)
=> ItemById(id, _publishedSnapshot.Media);
2018-06-29 19:52:40 +02:00
2022-02-22 13:35:32 +01:00
public IPublishedContent? Media(Udi? id)
2018-06-29 19:52:40 +02:00
{
2022-03-16 14:39:28 +01:00
if (!(id is GuidUdi udi))
{
return null;
}
2019-02-07 09:59:16 +01:00
return ItemById(udi.Guid, _publishedSnapshot.Media);
2018-06-29 19:52:40 +02:00
}
2022-02-22 13:35:32 +01:00
public IPublishedContent? Media(object id)
2018-06-29 19:52:40 +02:00
{
if (ConvertIdObjectToInt(id, out var intId))
{
return Media(intId);
}
if (ConvertIdObjectToGuid(id, out var guidId))
{
return Media(guidId);
}
if (ConvertIdObjectToUdi(id, out var udiId))
{
return Media(udiId);
}
return null;
2018-06-29 19:52:40 +02:00
}
public IEnumerable<IPublishedContent> Media(IEnumerable<int> ids)
=> ItemsByIds(_publishedSnapshot.Media, ids);
public IEnumerable<IPublishedContent> Media(IEnumerable<object> ids)
=> ids.Select(Media).WhereNotNull();
2018-06-29 19:52:40 +02:00
public IEnumerable<IPublishedContent> Media(IEnumerable<Guid> ids)
=> ItemsByIds(_publishedSnapshot.Media, ids);
2018-06-29 19:52:40 +02:00
public IEnumerable<IPublishedContent> MediaAtRoot()
=> ItemsAtRoot(_publishedSnapshot.Media);
2018-06-29 19:52:40 +02:00
#endregion
#region Used by Content/Media
private static IPublishedContent? ItemById(int id, IPublishedCache? cache)
=> cache?.GetById(id);
2018-06-29 19:52:40 +02:00
private static IPublishedContent? ItemById(Guid id, IPublishedCache? cache)
=> cache?.GetById(id);
2018-06-29 19:52:40 +02:00
private static IPublishedContent? ItemByXPath(string xpath, XPathVariable[] vars, IPublishedCache? cache)
=> cache?.GetSingleByXPath(xpath, vars);
2018-06-29 19:52:40 +02:00
private static IEnumerable<IPublishedContent> ItemsByIds(IPublishedCache? cache, IEnumerable<int> ids)
=> ids.Select(eachId => ItemById(eachId, cache)).WhereNotNull();
2018-06-29 19:52:40 +02:00
private IEnumerable<IPublishedContent> ItemsByIds(IPublishedCache? cache, IEnumerable<Guid> ids)
=> ids.Select(eachId => ItemById(eachId, cache)).WhereNotNull();
2018-06-29 19:52:40 +02:00
private static IEnumerable<IPublishedContent> ItemsByXPath(string xpath, XPathVariable[] vars, IPublishedCache? cache)
=> cache?.GetByXPath(xpath, vars) ?? Array.Empty<IPublishedContent>();
2018-06-29 19:52:40 +02:00
private static IEnumerable<IPublishedContent> ItemsByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedCache? cache)
=> cache?.GetByXPath(xpath, vars) ?? Array.Empty<IPublishedContent>();
2018-06-29 19:52:40 +02:00
private static IEnumerable<IPublishedContent> ItemsAtRoot(IPublishedCache? cache)
=> cache?.GetAtRoot() ?? Array.Empty<IPublishedContent>();
2018-06-29 19:52:40 +02:00
#endregion
#region Search
/// <inheritdoc />
public IEnumerable<PublishedSearchResult> Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName)
=> Search(term, 0, 0, out _, culture, indexName);
2018-06-29 19:52:40 +02:00
2021-02-16 20:56:44 +13:00
/// <inheritdoc />
2022-02-22 13:35:32 +01:00
public IEnumerable<PublishedSearchResult> Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet<string>? loadedFields = null)
2018-06-29 19:52:40 +02:00
{
2020-01-05 22:55:55 +01:00
if (skip < 0)
{
throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero.");
2020-01-05 22:55:55 +01:00
}
if (take < 0)
{
throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero.");
2020-01-05 22:55:55 +01:00
}
2019-11-01 10:49:45 +01:00
if (string.IsNullOrEmpty(indexName))
{
indexName = Constants.UmbracoIndexes.ExternalIndexName;
}
2018-06-29 19:52:40 +02:00
if (!_examineManager.TryGetIndex(indexName, out var index) || index is not IUmbracoIndex umbIndex)
2019-11-01 10:49:45 +01:00
{
throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}");
2019-11-01 10:49:45 +01:00
}
Examine 2.0 integration (#10241) * Init commit for examine 2.0 work, most old umb examine tests working, probably a lot that doesn't * Gets Umbraco Examine tests passing and makes some sense out of them, fixes some underlying issues. * Large refactor, remove TaskHelper, rename Notifications to be consistent, Gets all examine/lucene indexes building and startup ordered in the correct way, removes old files, creates new IUmbracoIndexingHandler for abstracting out all index operations for umbraco data, abstracts out IIndexRebuilder, Fixes Stack overflow with LiveModelsProvider and loading assemblies, ports some changes from v8 for startup handling with cold boots, refactors out LastSyncedFileManager * fix up issues with rebuilding and management dashboard. * removes old files, removes NetworkHelper, fixes LastSyncedFileManager implementation to ensure the machine name is used, fix up logging with cold boot state. * Makes MainDom safer to use and makes PublishedSnapshotService lazily register with MainDom * lazily acquire application id (fix unit tests) * Fixes resource casing and missing test file * Ensures caches when requiring internal services for PublishedSnapshotService, UseNuCache is a separate call, shouldn't be buried in AddWebComponents, was also causing issues in integration tests since nucache was being used for the Id2Key service. * For UmbracoTestServerTestBase enable nucache services * Fixing tests * Fix another test * Fixes tests, use TestHostingEnvironment, make Tests.Common use net5, remove old Lucene.Net.Contrib ref. * Fixes up some review notes * Fixes issue with doubly registering PublishedSnapshotService meanig there could be 2x instances of it * Checks for parseexception when executing the query * Use application root instead of duplicating functionality. * Added Examine project to netcore only solution file * Fixed casing issue with LazyLoad, that is not lowercase. * uses cancellationToken instead of bool flag, fixes always reading lastId from the LastSyncedFileManager, fixes RecurringHostedServiceBase so that there isn't an overlapping thread for the same task type * Fix tests * remove legacy test project from solution file * Fix test Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-05-18 18:31:38 +10:00
var query = umbIndex.Searcher.CreateQuery(IndexTypes.Content);
2018-12-20 13:29:57 +01:00
IOrdering ordering;
2019-07-12 13:15:41 +10:00
if (culture == "*")
{
2019-11-01 10:49:45 +01:00
// Search everything
ordering = query.ManagedQuery(term);
}
else if (string.IsNullOrWhiteSpace(culture))
2019-07-12 13:15:41 +10:00
{
// Only search invariant
ordering = query
.Field(UmbracoExamineFieldNames.VariesByCultureFieldName, "n") // Must not vary by culture
.And().ManagedQuery(term);
2019-07-12 13:15:41 +10:00
}
else
{
// Only search the specified culture
var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied
ordering = query.ManagedQuery(term, fields);
2021-02-16 20:56:44 +13:00
}
2018-06-29 19:52:40 +02:00
// Only select item ID field, because results are loaded from the published snapshot based on this single value
var queryExecutor = ordering.SelectFields(s_itemIdFieldNameHashSet);
var results = skip == 0 && take == 0
? queryExecutor.Execute()
Examine 2.0 integration (#10241) * Init commit for examine 2.0 work, most old umb examine tests working, probably a lot that doesn't * Gets Umbraco Examine tests passing and makes some sense out of them, fixes some underlying issues. * Large refactor, remove TaskHelper, rename Notifications to be consistent, Gets all examine/lucene indexes building and startup ordered in the correct way, removes old files, creates new IUmbracoIndexingHandler for abstracting out all index operations for umbraco data, abstracts out IIndexRebuilder, Fixes Stack overflow with LiveModelsProvider and loading assemblies, ports some changes from v8 for startup handling with cold boots, refactors out LastSyncedFileManager * fix up issues with rebuilding and management dashboard. * removes old files, removes NetworkHelper, fixes LastSyncedFileManager implementation to ensure the machine name is used, fix up logging with cold boot state. * Makes MainDom safer to use and makes PublishedSnapshotService lazily register with MainDom * lazily acquire application id (fix unit tests) * Fixes resource casing and missing test file * Ensures caches when requiring internal services for PublishedSnapshotService, UseNuCache is a separate call, shouldn't be buried in AddWebComponents, was also causing issues in integration tests since nucache was being used for the Id2Key service. * For UmbracoTestServerTestBase enable nucache services * Fixing tests * Fix another test * Fixes tests, use TestHostingEnvironment, make Tests.Common use net5, remove old Lucene.Net.Contrib ref. * Fixes up some review notes * Fixes issue with doubly registering PublishedSnapshotService meanig there could be 2x instances of it * Checks for parseexception when executing the query * Use application root instead of duplicating functionality. * Added Examine project to netcore only solution file * Fixed casing issue with LazyLoad, that is not lowercase. * uses cancellationToken instead of bool flag, fixes always reading lastId from the LastSyncedFileManager, fixes RecurringHostedServiceBase so that there isn't an overlapping thread for the same task type * Fix tests * remove legacy test project from solution file * Fix test Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-05-18 18:31:38 +10:00
: queryExecutor.Execute(QueryOptions.SkipTake(skip, take));
totalRecords = results.TotalItemCount;
return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture);
2018-06-29 19:52:40 +02:00
}
/// <inheritdoc />
public IEnumerable<PublishedSearchResult> Search(IQueryExecutor query)
=> Search(query, 0, 0, out _);
2018-06-29 19:52:40 +02:00
/// <inheritdoc />
public IEnumerable<PublishedSearchResult> Search(IQueryExecutor query, int skip, int take, out long totalRecords)
2018-06-29 19:52:40 +02:00
{
2020-01-05 22:55:55 +01:00
if (skip < 0)
{
throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero.");
2020-01-05 22:55:55 +01:00
}
if (take < 0)
{
throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero.");
}
if (query is IOrdering ordering)
{
// Only select item ID field, because results are loaded from the published snapshot based on this single value
query = ordering.SelectFields(s_itemIdFieldNameHashSet);
2020-01-05 22:55:55 +01:00
}
2018-06-29 19:52:40 +02:00
var results = skip == 0 && take == 0
? query.Execute()
Examine 2.0 integration (#10241) * Init commit for examine 2.0 work, most old umb examine tests working, probably a lot that doesn't * Gets Umbraco Examine tests passing and makes some sense out of them, fixes some underlying issues. * Large refactor, remove TaskHelper, rename Notifications to be consistent, Gets all examine/lucene indexes building and startup ordered in the correct way, removes old files, creates new IUmbracoIndexingHandler for abstracting out all index operations for umbraco data, abstracts out IIndexRebuilder, Fixes Stack overflow with LiveModelsProvider and loading assemblies, ports some changes from v8 for startup handling with cold boots, refactors out LastSyncedFileManager * fix up issues with rebuilding and management dashboard. * removes old files, removes NetworkHelper, fixes LastSyncedFileManager implementation to ensure the machine name is used, fix up logging with cold boot state. * Makes MainDom safer to use and makes PublishedSnapshotService lazily register with MainDom * lazily acquire application id (fix unit tests) * Fixes resource casing and missing test file * Ensures caches when requiring internal services for PublishedSnapshotService, UseNuCache is a separate call, shouldn't be buried in AddWebComponents, was also causing issues in integration tests since nucache was being used for the Id2Key service. * For UmbracoTestServerTestBase enable nucache services * Fixing tests * Fix another test * Fixes tests, use TestHostingEnvironment, make Tests.Common use net5, remove old Lucene.Net.Contrib ref. * Fixes up some review notes * Fixes issue with doubly registering PublishedSnapshotService meanig there could be 2x instances of it * Checks for parseexception when executing the query * Use application root instead of duplicating functionality. * Added Examine project to netcore only solution file * Fixed casing issue with LazyLoad, that is not lowercase. * uses cancellationToken instead of bool flag, fixes always reading lastId from the LastSyncedFileManager, fixes RecurringHostedServiceBase so that there isn't an overlapping thread for the same task type * Fix tests * remove legacy test project from solution file * Fix test Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-05-18 18:31:38 +10:00
: query.Execute(QueryOptions.SkipTake(skip, take));
2018-06-29 19:52:40 +02:00
totalRecords = results.TotalItemCount;
2019-11-01 10:49:45 +01:00
return results.ToPublishedSearchResults(_publishedSnapshot);
2018-06-29 19:52:40 +02:00
}
/// <summary>
/// This is used to contextualize the values in the search results when enumerating over them, so that the correct culture values are used.
/// </summary>
private class CultureContextualSearchResults : IEnumerable<PublishedSearchResult>
{
private readonly string _culture;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IEnumerable<PublishedSearchResult> _wrapped;
public CultureContextualSearchResults(IEnumerable<PublishedSearchResult> wrapped, IVariationContextAccessor variationContextAccessor, string culture)
{
_wrapped = wrapped;
_variationContextAccessor = variationContextAccessor;
_culture = culture;
}
public IEnumerator<PublishedSearchResult> GetEnumerator()
{
// We need to change the current culture to what is requested and then change it back
var originalContext = _variationContextAccessor.VariationContext;
2022-02-22 13:35:32 +01:00
if (!_culture.IsNullOrWhiteSpace() && !_culture.InvariantEquals(originalContext?.Culture))
{
_variationContextAccessor.VariationContext = new VariationContext(_culture);
}
// Now the IPublishedContent returned will be contextualized to the culture specified and will be reset when the enumerator is disposed
return new CultureContextualSearchResultsEnumerator(_wrapped.GetEnumerator(), _variationContextAccessor, originalContext);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Resets the variation context when this is disposed.
/// </summary>
private class CultureContextualSearchResultsEnumerator : IEnumerator<PublishedSearchResult>
{
2022-02-22 13:35:32 +01:00
private readonly VariationContext? _originalContext;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IEnumerator<PublishedSearchResult> _wrapped;
2022-03-16 14:39:28 +01:00
public CultureContextualSearchResultsEnumerator(
IEnumerator<PublishedSearchResult> wrapped,
IVariationContextAccessor variationContextAccessor,
VariationContext? originalContext)
{
_wrapped = wrapped;
_variationContextAccessor = variationContextAccessor;
_originalContext = originalContext;
}
public PublishedSearchResult Current => _wrapped.Current;
object IEnumerator.Current => Current;
public void Dispose()
{
_wrapped.Dispose();
// Reset to original variation context
_variationContextAccessor.VariationContext = _originalContext;
}
public bool MoveNext() => _wrapped.MoveNext();
public void Reset() => _wrapped.Reset();
}
}
2018-06-29 19:52:40 +02:00
#endregion
}
}