Skip to content

Commit

Permalink
Lots of new logic to support UDIs and build-in property value converters
Browse files Browse the repository at this point in the history
  • Loading branch information
abjerner committed Mar 22, 2018
1 parent f06ada0 commit 6eb55d4
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 43 deletions.
91 changes: 77 additions & 14 deletions src/Skybrud.UmbracoEssentials/Content/ContentUtils.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skybrud.Essentials.Strings.Extensions;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Xml;
using Umbraco.Web;

namespace Skybrud.UmbracoEssentials.Content {

/// <summary>
/// Static class with various utility methods for working with content.
/// </summary>
public static class ContentUtils {

#region Fields

/// <summary>
/// Gets a reference to the internal lookup table of GUIDs.
/// </summary>
public static readonly Dictionary<Guid, int> GuidLookupTable = new Dictionary<Guid, int>();

#endregion

#region Public methods

/// <summary>
/// Returns an instance of <see cref="IPublishedContent"/> from the content cache based on the ID specified by
/// <paramref name="str"/>.
/// </summary>
/// <param name="str">An instance of <see cref="String"/> with the ID of the content item.</param>
/// <returns>An instance of <see cref="IPublishedContent"/> if found, otherwise <code>NULL</code>.</returns>
/// <returns>An instance of <see cref="IPublishedContent"/> if found, otherwise <code>null</code>.</returns>
public static IPublishedContent TypedContent(string str) {

// Get the first ID in a comma separated string
int contentId = str.CsvToInt().FirstOrDefault();
if (UmbracoContext.Current == null) return null;
if (String.IsNullOrWhiteSpace(str)) return null;

// Iterate through each ID (there may be more than one depending on property type)
foreach (string id in str.Split(',', ' ', '\r', '\n', '\t')) {
IPublishedContent content = TypedDocumentById(id);
if (content != null) return content;
}

// Parse the value and attempt to find the content node in the cache
return contentId > 0 && UmbracoContext.Current != null ? UmbracoContext.Current.ContentCache.GetById(contentId) : null;
return null;

}

Expand All @@ -31,11 +53,11 @@ public static IPublishedContent TypedContent(string str) {
/// </summary>
/// <param name="str">An instance of <see cref="String"/> with the ID of the content item.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>An instance of <typeparamref name="T"/> if found, otherwise <code>NULL</code>.</returns>
/// <returns>An instance of <typeparamref name="T"/> if found, otherwise <code>null</code>.</returns>
public static T TypedContent<T>(string str, Func<IPublishedContent, T> func) {

// A callback must be specified
if (func == null) throw new ArgumentNullException("func");
if (func == null) throw new ArgumentNullException(nameof(func));

// Find the content using the method overload
IPublishedContent content = TypedContent(str);
Expand All @@ -55,11 +77,14 @@ public static IPublishedContent[] TypedCsvContent(string str) {

// If the Umbraco context isn't avaiable, we just return an empty array
if (UmbracoContext.Current == null) return new IPublishedContent[0];

// Also just return an empty array if the string is either NULL or empty
if (String.IsNullOrWhiteSpace(str)) return new IPublishedContent[0];

// Look up each ID in the content cache and return the collection as an array
return (
from id in str.CsvToInt()
let item = UmbracoContext.Current.ContentCache.GetById(id)
from id in str.Split(',', ' ', '\r', '\n', '\t')
let item = TypedDocumentById(id)
where item != null
select item
).ToArray();
Expand All @@ -72,25 +97,63 @@ select item
/// </summary>
/// <param name="str">An instance of <see cref="String"/> with the comma separated IDs of the content items.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>Array of <typeparamref name="T"/>.</returns>
/// <returns>An array of <typeparamref name="T"/>.</returns>
public static T[] TypedCsvContent<T>(string str, Func<IPublishedContent, T> func) {

// A callback must be specified
if (func == null) throw new ArgumentNullException("func");
if (func == null) throw new ArgumentNullException(nameof(func));

// If the Umbraco context isn't avaiable, we just return an empty array
if (UmbracoContext.Current == null) return new T[0];

// Look up each ID in the content cache and return the collection as an array
return (
from id in str.CsvToInt()
let item = UmbracoContext.Current.ContentCache.GetById(id)
from id in str.Split(',', ' ', '\r', '\n', '\t')
let item = TypedDocumentById(id)
where item != null
select func(item)
).ToArray();

}

#endregion

#region Private helper methods

private static IPublishedContent TypedDocumentById(string id) {
if (String.IsNullOrWhiteSpace(id)) return null;
if (Guid.TryParse(id.Replace("umb://document/", ""), out Guid guid)) return TypedDocumentById(guid);
if (Int32.TryParse(id, out Int32 numeric)) return UmbracoContext.Current.ContentCache.GetById(numeric);
return null;
}

/// <see>
/// <cref>https://github.com/umbraco/Umbraco-CMS/blob/3d90c2b83f76d398f28500e35cacd5944f5c1971/src/Umbraco.Web/PublishedContentQuery.cs#L235</cref>
/// </see>
private static IPublishedContent TypedDocumentById(Guid guid) {

// Look op the content item by it's ID if in the lookup table
if (GuidLookupTable.TryGetValue(guid, out int id)) {
return UmbracoContext.Current.ContentCache.GetById(id);
}

// TODO: Fix so we don't use expesnsive XPath queries (Umbraco must support this first)

var legacyXml = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema;
var xpath = legacyXml ? "//node [@key=$guid]" : "//* [@key=$guid]";
var doc = UmbracoContext.Current.ContentCache.GetSingleByXPath(xpath, new XPathVariable("guid", guid.ToString()));

if (doc == null) return null;

// Add the GUID to the lookup table
GuidLookupTable[guid] = doc.Id;

return doc;

}

#endregion

}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skybrud.UmbracoEssentials.Content;
using Umbraco.Core.Models;
using Umbraco.Web;
Expand All @@ -7,20 +9,98 @@ namespace Skybrud.UmbracoEssentials.Extensions.PublishedContent {

public static partial class PublishedContentExtensions {

/// <summary>
/// Returns an instance of <see cref="IPublishedContent"/> from the content cache based on the ID specified in
/// the property with the specified <paramref name="propertyAlias"/>.
/// </summary>
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the ID.</param>
/// <param name="recursive">A value indicating whether to recurse.</param>
/// <returns>Instance of <see cref="IPublishedContent"/> if found, otherwise <code>null</code>.</returns>
public static IPublishedContent TypedContent(this IPublishedContent content, string propertyAlias, bool recursive = false) {
return ContentUtils.TypedContent(content.GetPropertyValue<string>(propertyAlias, recursive) ?? "");

// Get the property value
object propertyValue = content?.GetPropertyValue(propertyAlias, recursive);
if (propertyValue == null) return null;

// Handle various value types
switch (propertyValue) {

case IPublishedContent pc:
return pc;

case List<IPublishedContent> lc:
return lc.FirstOrDefault();

case string str:
return ContentUtils.TypedContent(str);

default:
return null;

}

}

/// <summary>
/// Returns an instance of <see cref="IPublishedContent"/> from the content cache based on the ID specified in
/// the property with the specified <paramref name="propertyAlias"/>. If found, the
/// <see cref="IPublishedContent"/> is converted to the type of <typeparamref name="T"/> using the specified
/// <paramref name="func"/>.
/// </summary>
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the ID.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>Instance of <typeparamref name="T"/> if found, otherwise <code>default(T)</code>.</returns>
public static T TypedContent<T>(this IPublishedContent content, string propertyAlias, Func<IPublishedContent, T> func) {
return ContentUtils.TypedContent(content.GetPropertyValue<string>(propertyAlias) ?? "", func);
IPublishedContent item = TypedContent(content, propertyAlias);
return item == null ? default(T) : func(item);
}

/// <summary>
/// Converts the comma seperated IDs of the property with the specified <paramref name="propertyAlias"/> into
/// an array of <see cref="IPublishedContent"/> by using the content cache.
/// </summary>
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the IDs.</param>
/// <param name="recursive">A value indicating whether to recurse.</param>
/// <returns>Array of <see cref="IPublishedContent"/>.</returns>
public static IPublishedContent[] TypedCsvContent(this IPublishedContent content, string propertyAlias, bool recursive = false) {
return ContentUtils.TypedCsvContent(content.GetPropertyValue<string>(propertyAlias, recursive) ?? "");

// Get the property value
object propertyValue = content?.GetPropertyValue(propertyAlias, recursive);
if (propertyValue == null) return null;

// Handle various value types
switch (propertyValue) {

case IPublishedContent pc:
return new []{ pc };

case List<IPublishedContent> lc:
return lc.ToArray();

case string str:
return ContentUtils.TypedCsvContent(str);

default:
return new IPublishedContent[0];

}

}

/// <summary>
/// Converts the comma seperated IDs of the property with the specified <paramref name="propertyAlias"/> into
/// an array of <typeparamref name="T"/> by using the content cache. Each content item is converted to the type
/// of <typeparamref name="T"/> using the specified <paramref name="func"/>.
/// </summary>
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the IDs.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>Array of <typeparamref name="T"/>.</returns>
public static T[] TypedCsvContent<T>(this IPublishedContent content, string propertyAlias, Func<IPublishedContent, T> func) {
return ContentUtils.TypedCsvContent(content.GetPropertyValue<string>(propertyAlias) ?? "", func);
return TypedCsvContent(content, propertyAlias).Select(func).ToArray();
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skybrud.UmbracoEssentials.Media;
using Umbraco.Core.Models;
using Umbraco.Web;
Expand All @@ -14,9 +16,30 @@ public static partial class PublishedContentExtensions {
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the ID.</param>
/// <param name="recursive">A value indicating whether to recurse.</param>
/// <returns>Instance of <see cref="IPublishedContent"/> if found, otherwise <code>NULL</code>.</returns>
/// <returns>Instance of <see cref="IPublishedContent"/> if found, otherwise <code>null</code>.</returns>
public static IPublishedContent TypedMedia(this IPublishedContent content, string propertyAlias, bool recursive = false) {
return MediaUtils.TypedMedia(content.GetPropertyValue<string>(propertyAlias, recursive) ?? "");

// Get the property value
object propertyValue = content?.GetPropertyValue(propertyAlias, recursive);
if (propertyValue == null) return null;

// Handle various value types
switch (propertyValue) {

case IPublishedContent pc:
return pc;

case List<IPublishedContent> lc:
return lc.FirstOrDefault();

case string str:
return MediaUtils.TypedMedia(str);

default:
return null;

}

}

/// <summary>
Expand All @@ -28,9 +51,10 @@ public static IPublishedContent TypedMedia(this IPublishedContent content, strin
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the ID.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>Instance of <typeparamref name="T"/> if found, otherwise <code>NULL</code>.</returns>
/// <returns>Instance of <typeparamref name="T"/> if found, otherwise <code>default(T)</code>.</returns>
public static T TypedMedia<T>(this IPublishedContent content, string propertyAlias, Func<IPublishedContent, T> func) {
return MediaUtils.TypedMedia(content.GetPropertyValue<string>(propertyAlias) ?? "", func);
IPublishedContent item = TypedMedia(content, propertyAlias);
return item == null ? default(T) : func(item);
}

/// <summary>
Expand All @@ -42,20 +66,41 @@ public static T TypedMedia<T>(this IPublishedContent content, string propertyAli
/// <param name="recursive">A value indicating whether to recurse.</param>
/// <returns>Array of <see cref="IPublishedContent"/>.</returns>
public static IPublishedContent[] TypedCsvMedia(this IPublishedContent content, string propertyAlias, bool recursive = false) {
return MediaUtils.TypedCsvMedia(content.GetPropertyValue<string>(propertyAlias, recursive) ?? "");

// Get the property value
object propertyValue = content?.GetPropertyValue(propertyAlias, recursive);
if (propertyValue == null) return null;

// Handle various value types
switch (propertyValue) {

case IPublishedContent pc:
return new []{ pc };

case List<IPublishedContent> lc:
return lc.ToArray();

case string str:
return MediaUtils.TypedCsvMedia(str);

default:
return new IPublishedContent[0];

}

}

/// <summary>
/// Converts the comma seperated IDs of the property with the specified <paramref name="propertyAlias"/> into
/// an array of <code>T</code> by using the media cache. Each media is converted to the type of <code>T</code>
/// using the specified <paramref name="func"/>.
/// an array of <typeparamref name="T"/> by using the media cache. Each media is converted to the type of
/// <typeparamref name="T"/> using the specified <paramref name="func"/>.
/// </summary>
/// <param name="content">An instance of <see cref="IPublishedContent"/>.</param>
/// <param name="propertyAlias">The alias of the property containing the IDs.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>Array of <typeparamref name="T"/>.</returns>
public static T[] TypedCsvMedia<T>(this IPublishedContent content, string propertyAlias, Func<IPublishedContent, T> func) {
return MediaUtils.TypedCsvMedia(content.GetPropertyValue<string>(propertyAlias) ?? "", func);
return TypedCsvMedia(content, propertyAlias).Select(func).ToArray();
}

}
Expand Down
Loading

0 comments on commit 6eb55d4

Please sign in to comment.