diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 45901847eb..58804beac5 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -100,7 +100,7 @@ namespace Umbraco.Core // public bool IsConfigured { - // fixme - we should not do this - ok for now + // todo - we should not do this - ok for now get { return Configured; diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 54f9ab4832..fc17bd7245 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -47,6 +47,5 @@ namespace Umbraco.Core.Cache public const string StylesheetPropertyCacheKey = "UmbracoStylesheetProperty"; public const string DataTypeCacheKey = "UmbracoDataTypeDefinition"; - } } \ No newline at end of file diff --git a/src/Umbraco.Core/CacheHelper.cs b/src/Umbraco.Core/CacheHelper.cs index 8bb80670d8..0de92e1259 100644 --- a/src/Umbraco.Core/CacheHelper.cs +++ b/src/Umbraco.Core/CacheHelper.cs @@ -74,6 +74,14 @@ namespace Umbraco.Core { } + internal void ClearStaticCacheObjectTypes(Func predicate) + { + if (_enableCache) + _staticCache.ClearCacheObjectTypes(predicate); + else + _nullStaticCache.ClearCacheObjectTypes(predicate); + } + /// /// Private ctor used for creating a disabled cache helper /// diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 243ea71f52..14e69597f0 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; @@ -17,6 +18,7 @@ using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Publishing; using Umbraco.Core.Macros; using Umbraco.Core.Services; @@ -308,13 +310,16 @@ namespace Umbraco.Core //the database migration objects MigrationResolver.Current = new MigrationResolver( () => PluginManager.Current.ResolveMigrationTypes()); - - //NOTE: These are legacy in v7+ and will eventually need to be removed. - PropertyEditorValueConvertersResolver.Current = new PropertyEditorValueConvertersResolver( + + // todo: remove once we drop IPropertyEditorValueConverter support. + PropertyEditorValueConvertersResolver.Current = new PropertyEditorValueConvertersResolver( PluginManager.Current.ResolvePropertyEditorValueConverters()); + //add the internal ones, these are not public currently so need to add them manually PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver( PluginManager.Current.ResolvePropertyValueConverters()); + // fixme - why not use the following syntax? + //PluginManager.Current.ResolveTypes()); // this is how we'd switch over to DefaultShortStringHelper _and_ still use // UmbracoSettings UrlReplaceCharacters... @@ -327,6 +332,9 @@ namespace Umbraco.Core UrlSegmentProviderResolver.Current = new UrlSegmentProviderResolver( typeof (DefaultUrlSegmentProvider)); + + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver( + new PublishedContentModelFactoryImpl()); } } } diff --git a/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs b/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs index 1338ee03e2..91004fa114 100644 --- a/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs +++ b/src/Umbraco.Core/Dynamics/DynamicInstanceHelper.cs @@ -112,7 +112,8 @@ namespace Umbraco.Core.Dynamics { //don't log here, we return this exception because the caller may need to do something specific when //this exception occurs. - return Attempt.Fail(ext); + var mresult = new TryInvokeMemberResult(null, TryInvokeMemberSuccessReason.FoundExtensionMethod); + return Attempt.Fail(mresult, ext); } catch (Exception ex) { @@ -124,7 +125,8 @@ namespace Umbraco.Core.Dynamics sb.Append(t + ","); } LogHelper.Error(sb.ToString(), ex); - return Attempt.Fail(ex); + var mresult = new TryInvokeMemberResult(null, TryInvokeMemberSuccessReason.FoundExtensionMethod); + return Attempt.Fail(mresult, ex); } } return Attempt.Fail(); diff --git a/src/Umbraco.Core/Dynamics/DynamicNull.cs b/src/Umbraco.Core/Dynamics/DynamicNull.cs index 0788e68923..593808e867 100644 --- a/src/Umbraco.Core/Dynamics/DynamicNull.cs +++ b/src/Umbraco.Core/Dynamics/DynamicNull.cs @@ -11,78 +11,101 @@ namespace Umbraco.Core.Dynamics //Because it's IEnumerable, if the user is actually trying @Model.TextPages or similar //it will still return an enumerable object (assuming the call actually failed because there were no children of that type) //but in .Where, if they use a property that doesn't exist, the lambda will bypass this and return false + + // returned when TryGetMember fails on a DynamicPublishedContent + // + // so if user does @CurrentPage.TextPages it will get something that is enumerable (but empty) + // note - not sure I understand the stuff about .Where, though + public class DynamicNull : DynamicObject, IEnumerable, IHtmlString { + public static readonly DynamicNull Null = new DynamicNull(); + + private DynamicNull() {} + public IEnumerator GetEnumerator() { return (new List()).GetEnumerator(); } + public DynamicNull Where(string predicate, params object[] values) { return this; } + public DynamicNull OrderBy(string orderBy) { return this; } + + public DynamicNull ToContentSet() + { + return this; + } + public int Count() { return 0; } + public override string ToString() { return string.Empty; } + public override bool TryGetMember(GetMemberBinder binder, out object result) { result = this; return true; } + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) { result = this; return true; } + public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) { result = this; return true; } + public bool IsNull() { return true; } + public bool HasValue() { return false; } + public string Name { - get - { - return string.Empty; - } + get { return string.Empty; } } + public int Id { - get - { - return 0; - } + get { return 0; } } public static implicit operator bool(DynamicNull n) { return false; } + public static implicit operator DateTime(DynamicNull n) { return DateTime.MinValue; } + public static implicit operator int(DynamicNull n) { return 0; } + public static implicit operator string(DynamicNull n) { return string.Empty; @@ -92,6 +115,5 @@ namespace Umbraco.Core.Dynamics { return string.Empty; } - } } diff --git a/src/Umbraco.Core/Dynamics/DynamicXml.cs b/src/Umbraco.Core/Dynamics/DynamicXml.cs index a84389d2e5..7a4d714fbe 100644 --- a/src/Umbraco.Core/Dynamics/DynamicXml.cs +++ b/src/Umbraco.Core/Dynamics/DynamicXml.cs @@ -203,7 +203,7 @@ namespace Umbraco.Core.Dynamics if (attempt.Result.Reason == DynamicInstanceHelper.TryInvokeMemberSuccessReason.FoundExtensionMethod && attempt.Exception != null && attempt.Exception is TargetInvocationException) { - result = new DynamicNull(); + result = DynamicNull.Null; return true; } diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index 2fe86ddeec..cfc8a92ddf 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -13,7 +13,96 @@ namespace Umbraco.Core.Dynamics /// Utility class for finding extension methods on a type to execute /// internal static class ExtensionMethodFinder - { + { + private static readonly MethodInfo[] AllExtensionMethods; + + static ExtensionMethodFinder() + { + AllExtensionMethods = TypeFinder.GetAssembliesWithKnownExclusions() + // assemblies that contain extension methods + .Where(a => a.IsDefined(typeof(ExtensionAttribute), false)) + // types that contain extension methods + .SelectMany(a => a.GetTypes() + .Where(t => t.IsDefined(typeof(ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) + // actual extension methods + .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(m => m.IsDefined(typeof(ExtensionAttribute), false))) + // and also IEnumerable extension methods - because the assembly is excluded + .Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) + .ToArray(); + } + + // ORIGINAL CODE IS NOT COMPLETE, DOES NOT HANDLE GENERICS, ETC... + + // so this is an attempt at fixing things, but it's not done yet + // and do we really want to do this? extension methods are not supported on dynamics, period + // we should use strongly typed content instead of dynamics. + + /* + + // get all extension methods for type thisType, with name name, + // accepting argsCount arguments (not counting the instance of thisType). + private static IEnumerable GetExtensionMethods(Type thisType, string name, int argsCount) + { + var key = string.Format("{0}.{1}::{2}", thisType.FullName, name, argsCount); + + var types = thisType.GetBaseTypes(true); // either do this OR have MatchFirstParameter handle the stuff... F*XME + + var methods = AllExtensionMethods + .Where(m => m.Name == name) + .Where(m => m.GetParameters().Length == argsCount) + .Where(m => MatchFirstParameter(thisType, m.GetParameters()[0].ParameterType)); + + // f*xme - is this what we should cache? + return methods; + } + + // find out whether the first parameter is a match for thisType + private static bool MatchFirstParameter(Type thisType, Type firstParameterType) + { + return MethodArgZeroHasCorrectTargetType(null, firstParameterType, thisType); + } + + // get the single extension method for type thisType, with name name, + // that accepts the arguments in args (which does not contain the instance of thisType). + public static MethodInfo GetExtensionMethod(Type thisType, string name, object[] args) + { + MethodInfo result = null; + foreach (var method in GetExtensionMethods(thisType, name, args.Length).Where(m => MatchParameters(m, args))) + { + if (result == null) + result = method; + else + throw new AmbiguousMatchException("More than one matching extension method was found."); + } + return result; + } + + // find out whether the method can accept the arguments + private static bool MatchParameters(MethodInfo method, IList args) + { + var parameters = method.GetParameters(); + + var i = 0; + for (; i < parameters.Length; ++i) + { + if (MatchParameter(parameters[i].ParameterType, args[i].GetType()) == false) + break; + } + return (i == parameters.Length); + } + + internal static bool MatchParameter(Type parameterType, Type argumentType) + { + // public static int DoSomething(Foo foo, T t1, T t2) + // DoSomething(foo, t1, t2) => how can we match?! + return parameterType == argumentType; // f*xme of course! + } + * + */ + + // BELOW IS THE ORIGINAL CODE... + /// /// Returns all extension methods found matching the definition /// @@ -27,6 +116,10 @@ namespace Umbraco.Core.Dynamics /// private static IEnumerable GetAllExtensionMethods(Type thisType, string name, int argumentCount, bool argsContainsThis) { + // at *least* we can cache the extension methods discovery + var candidates = AllExtensionMethods; + + /* //only scan assemblies we know to contain extension methods (user assemblies) var assembliesToScan = TypeFinder.GetAssembliesWithKnownExclusions(); @@ -45,6 +138,7 @@ namespace Umbraco.Core.Dynamics //add the extension methods defined in IEnumerable candidates = candidates.Concat(typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)); + */ //filter by name var methodsByName = candidates.Where(m => m.Name == name); diff --git a/src/Umbraco.Core/Dynamics/PropertyResult.cs b/src/Umbraco.Core/Dynamics/PropertyResult.cs index 97d93a653e..e7cfed3f4b 100644 --- a/src/Umbraco.Core/Dynamics/PropertyResult.cs +++ b/src/Umbraco.Core/Dynamics/PropertyResult.cs @@ -1,60 +1,46 @@ using System; using Umbraco.Core.Models; -using umbraco.interfaces; using System.Web; namespace Umbraco.Core.Dynamics { - internal class PropertyResult : IPublishedContentProperty, IHtmlString - { - internal PropertyResult(IPublishedContentProperty source, PropertyResultType type) + internal class PropertyResult : IPublishedProperty, IHtmlString + { + private readonly IPublishedProperty _source; + private readonly string _alias; + private readonly object _value; + private readonly PropertyResultType _type; + + internal PropertyResult(IPublishedProperty source, PropertyResultType type) { if (source == null) throw new ArgumentNullException("source"); - - Alias = source.Alias; - Value = source.Value; - PropertyType = type; + + _type = type; + _source = source; } + internal PropertyResult(string alias, object value, PropertyResultType type) { if (alias == null) throw new ArgumentNullException("alias"); if (value == null) throw new ArgumentNullException("value"); - Alias = alias; - Value = value; - PropertyType = type; + _type = type; + _alias = alias; + _value = value; } - internal PropertyResultType PropertyType { get; private set; } - - public string Alias { get; private set; } + internal PropertyResultType PropertyType { get { return _type; } } - public object Value { get; private set; } - - /// - /// Returns the value as a string output, this is used in the final rendering process of a property - /// - internal string ValueAsString - { - get - { - return Value == null ? "" : Convert.ToString(Value); - } - } - - /// - /// The Id of the document for which this property belongs to - /// - public int DocumentId { get; set; } - - /// - /// The alias of the document type alias for which this property belongs to - /// - public string DocumentTypeAlias { get; set; } + public string PropertyTypeAlias { get { return _source == null ? _alias : _source.PropertyTypeAlias; } } + public object DataValue { get { return _source == null ? _value : _source.DataValue; } } + public bool HasValue { get { return _source == null || _source.HasValue; } } + public object ObjectValue { get { return _source == null ? _value : _source.ObjectValue; } } + public object XPathValue { get { return ObjectValue == null ? null : ObjectValue.ToString(); } } public string ToHtmlString() { - return ValueAsString; + var value = ObjectValue; + return value == null ? string.Empty : value.ToString(); } } } diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 3fa4847a26..a63fa2c1ba 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -243,23 +243,45 @@ namespace Umbraco.Core }); } - ///Finds the index of the first item matching an expression in an enumerable. - ///The enumerable to search. - ///The expression to test the items against. - ///The index of the first matching item, or -1 if no items match. + /// + /// Finds the index of the first item matching an expression in an enumerable. + /// + /// The type of the enumerated objects. + /// The enumerable to search. + /// The expression to test the items against. + /// The index of the first matching item, or -1. public static int FindIndex(this IEnumerable items, Func predicate) + { + return FindIndex(items, 0, predicate); + } + + /// + /// Finds the index of the first item matching an expression in an enumerable. + /// + /// The type of the enumerated objects. + /// The enumerable to search. + /// The index to start at. + /// The expression to test the items against. + /// The index of the first matching item, or -1. + public static int FindIndex(this IEnumerable items, int startIndex, Func predicate) { if (items == null) throw new ArgumentNullException("items"); if (predicate == null) throw new ArgumentNullException("predicate"); + if (startIndex < 0) throw new ArgumentOutOfRangeException("startIndex"); + + var index = startIndex; + if (index > 0) + items = items.Skip(index); - var retVal = 0; foreach (var item in items) { - if (predicate(item)) return retVal; - retVal++; + if (predicate(item)) return index; + index++; } + return -1; } + ///Finds the index of the first occurence of an item in an enumerable. ///The enumerable to search. ///The item to find. diff --git a/src/Umbraco.Core/Models/IPublishedContent.cs b/src/Umbraco.Core/Models/IPublishedContent.cs index 6f55bdb413..854ddfd47d 100644 --- a/src/Umbraco.Core/Models/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/IPublishedContent.cs @@ -1,20 +1,48 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models { /// - /// Defines a published item in Umbraco + /// Represents a cached content. /// /// - /// A replacement for INode which needs to occur since INode doesn't contain the document type alias - /// and INode is poorly formatted with mutable properties (i.e. Lists instead of IEnumerable) + /// SD: A replacement for INode which needs to occur since INode doesn't contain the document type alias + /// and INode is poorly formatted with mutable properties (i.e. Lists instead of IEnumerable). + /// Stephan: initially, that was for cached published content only. Now, we're using it also for + /// cached preview (so, maybe unpublished) content. A better name would therefore be ICachedContent, as + /// has been suggested. However, can't change now. Maybe in v7? /// - public interface IPublishedContent - { - int Id { get; } + public interface IPublishedContent + { + #region ContentSet + + // Because of http://issues.umbraco.org/issue/U4-1797 and in order to implement + // Index() and methods that derive from it such as IsFirst(), IsLast(), etc... all + // content items must know about their containing content set. + + /// + /// Gets the content set to which the content belongs. + /// + /// The default set consists in the siblings of the content (including the content + /// itself) ordered by sortOrder. + IEnumerable ContentSet { get; } + + #endregion + + #region ContentType + + /// + /// Gets the content type. + /// + PublishedContentType ContentType { get; } + + #endregion + + #region Content + + int Id { get; } int TemplateId { get; } int SortOrder { get; } string Name { get; } @@ -31,32 +59,97 @@ namespace Umbraco.Core.Models Guid Version { get; } int Level { get; } string Url { get; } + + /// + /// Gets a value indicating whether the content is a content (aka a document) or a media. + /// PublishedItemType ItemType { get; } + + /// + /// Gets a value indicating whether the content is draft. + /// + /// A content is draft when it is the unpublished version of a content, which may + /// have a published version, or not. + bool IsDraft { get; } + + /// + /// Gets the index of the published content within its current owning content set. + /// + /// The index of the published content within its current owning content set. + int GetIndex(); + + #endregion + + #region Tree + + /// + /// Gets the parent of the content. + /// + /// The parent of root content is null. IPublishedContent Parent { get; } + + /// + /// Gets the children of the content. + /// + /// Children are sorted by their sortOrder. IEnumerable Children { get; } - ICollection Properties { get; } + #endregion - /// - /// Returns the property value for the property alias specified - /// - /// - /// - object this[string propertyAlias] { get; } + #region Properties - /// - /// Returns a property on the object based on an alias + /// + /// Gets the properties of the content. + /// + /// + /// Contains one IPublishedProperty for each property defined for the content type, including + /// inherited properties. Some properties may have no value. + /// The properties collection of an IPublishedContent instance should be read-only ie it is illegal + /// to add properties to the collection. + /// + ICollection Properties { get; } + + /// + /// Gets a property identified by its alias. + /// + /// The property alias. + /// The property identified by the alias. + /// + /// If the content type has no property with that alias, including inherited properties, returns null, + /// otherwise return a property -- that may have no value (ie HasValue is false). + /// The alias is case-insensitive. + /// + IPublishedProperty GetProperty(string alias); + + /// + /// Gets a property identified by its alias. + /// + /// The property alias. + /// A value indicating whether to navigate the tree upwards until a property with a value is found. + /// The property identified by the alias. + /// + /// Navigate the tree upwards and look for a property with that alias and with a value (ie HasValue is true). + /// If found, return the property. If no property with that alias is found, having a value or not, return null. Otherwise + /// return the first property that was found with the alias but had no value (ie HasValue is false). + /// The alias is case-insensitive. + /// + IPublishedProperty GetProperty(string alias, bool recurse); + + /// + /// Gets the value of a property identified by its alias. /// - /// - /// - /// - /// Although we do have a a property to return Properties of the object, in some cases a custom implementation may not know - /// about all properties until specifically asked for one by alias. - /// - /// This method is mostly used in places such as DynamicPublishedContent when trying to resolve a property based on an alias. - /// In some cases Pulish Stores, a property value may exist in multiple places and we need to fallback to different cached locations - /// therefore sometimes the 'Properties' collection may not be sufficient. - /// - IPublishedContentProperty GetProperty(string alias); - } + /// The property alias. + /// The value of the property identified by the alias. + /// + /// If GetProperty(alias) is null then returns null else return GetProperty(alias).Value. + /// So if the property has no value, returns the default value for that property type. + /// This one is defined here really because we cannot define index extension methods, but all it should do is: + /// var p = GetProperty(alias); return p == null ? null : p.Value; and nothing else. + /// The recursive syntax (eg "_title") is _not_ supported here. + /// The alias is case-insensitive. + /// + object this[string alias] { get; } // todo - should obsolete this[alias] (when?) + + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IPublishedContentProperty.cs b/src/Umbraco.Core/Models/IPublishedContentProperty.cs deleted file mode 100644 index 1651e12f04..0000000000 --- a/src/Umbraco.Core/Models/IPublishedContentProperty.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Umbraco.Core.Models -{ - public interface IPublishedContentProperty - { - string Alias { get; } - object Value { get; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IPublishedProperty.cs b/src/Umbraco.Core/Models/IPublishedProperty.cs new file mode 100644 index 0000000000..45ada63e17 --- /dev/null +++ b/src/Umbraco.Core/Models/IPublishedProperty.cs @@ -0,0 +1,60 @@ +namespace Umbraco.Core.Models +{ + /// + /// Represents a property of an IPublishedContent. + /// + public interface IPublishedProperty + { + /// + /// Gets the alias of the property. + /// + string PropertyTypeAlias { get; } + + /// + /// Gets a value indicating whether the property has a value. + /// + /// + /// This is somewhat implementation-dependent -- depending on whatever IPublishedCache considers + /// a missing value. + /// The XmlPublishedCache raw values are strings, and it will consider missing, null or empty (and + /// that includes whitespace-only) strings as "no value". + /// Other caches that get their raw value from the database would consider that a property has "no + /// value" if it is missing, null, or an empty string (including whitespace-only). + /// + bool HasValue { get; } + + /// + /// Gets the data value of the property. + /// + /// + /// The data value is whatever was passed to the property when it was instanciated, and it is + /// somewhat implementation-dependent -- depending on how the IPublishedCache is implemented. + /// The XmlPublishedCache raw values are strings exclusively since they come from the Xml cache. + /// For other caches that get their raw value from the database, it would be either a string, + /// an integer (Int32), or a date and time (DateTime). + /// If you're using that value, you're probably wrong, unless you're doing some internal + /// Umbraco stuff. + /// + object DataValue { get; } + + /// + /// Gets the object value of the property. + /// + /// + /// The value is what you want to use when rendering content in an MVC view ie in C#. + /// It can be null, or any type of CLR object. + /// It has been fully prepared and processed by the appropriate converter. + /// + object ObjectValue { get; } + + /// + /// Gets the XPath value of the property. + /// + /// + /// The XPath value is what you want to use when navigating content via XPath eg in the XSLT engine. + /// It must be either null, or a string, or an XPathNavigator. + /// It has been fully prepared and processed by the appropriate converter. + /// + object XPathValue { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentExtended.cs new file mode 100644 index 0000000000..1a05c2e07a --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentExtended.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides methods to handle extended content. + /// + internal interface IPublishedContentExtended : IPublishedContent + { + /// + /// Adds a property to the extended content. + /// + /// The property to add. + void AddProperty(IPublishedProperty property); + + /// + /// Gets a value indicating whether properties were added to the extended content. + /// + bool HasAddedProperties { get; } + + /// + /// Sets the content set of the extended content. + /// + /// + void SetContentSet(IEnumerable contentSet); + + /// + /// Resets the content set of the extended content. + /// + void ClearContentSet(); + + /// + /// Sets the index of the extended content. + /// + /// The index value. + void SetIndex(int value); + + /// + /// Resets the index of the extended content. + /// + void ClearIndex(); + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentModelFactory.cs new file mode 100644 index 0000000000..cd58a43b54 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentModelFactory.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides the model creation service. + /// + internal interface IPublishedContentModelFactory + { + /// + /// Creates a strongly-typed model representing a published content. + /// + /// The original published content. + /// The strongly-typed model representing the published content, or the published content + /// itself it the factory has no model for that content type. + IPublishedContent CreateModel(IPublishedContent content); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs new file mode 100644 index 0000000000..37c9ab3203 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtended.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Umbraco.Core.Models.PublishedContent +{ + + public class PublishedContentExtended : PublishedContentWrapped, IPublishedContentExtended + { + #region Constructor + + // protected for models, private for our static Extend method + protected PublishedContentExtended(IPublishedContent content) + : base(content) + { } + + #endregion + + #region Index + + private int? _index; + + public override int GetIndex() + { + // fast + if (_index.HasValue) return _index.Value; + + // slow -- and don't cache, not in a set + if (_contentSet == null) return Content.GetIndex(); + + // slow -- but cache for next time + var index = _contentSet.FindIndex(x => x.Id == Id); + if (index < 0) + throw new IndexOutOfRangeException("Could not find content in the content set."); + _index = index; + return index; + } + + #endregion + + #region Extend + + internal static IPublishedContentExtended Extend(IPublishedContent content, IEnumerable contentSet) + { + var wrapped = content as PublishedContentExtended; + while (wrapped != null && ((IPublishedContentExtended)wrapped).HasAddedProperties == false) + wrapped = (content = wrapped.Unwrap()) as PublishedContentExtended; + + // if the factory returns something else than content it means it has created + // a model, and then that model has to inherit from PublishedContentExtended, + // => implements the internal IPublishedContentExtended. + + var model = PublishedContentModelFactory.CreateModel(content); + var extended = model == content // == means the factory did not create a model + ? new PublishedContentExtended(content) // so we have to extend + : model; // else we can use what the factory returned + + var extended2 = extended as IPublishedContentExtended; + if (extended2 != null) // always true, but keeps Resharper happy + extended2.SetContentSet(contentSet); + return extended2; + } + + #endregion + + #region IPublishedContentExtended + + void IPublishedContentExtended.AddProperty(IPublishedProperty property) + { + if (_properties == null) + _properties = new Collection(); + _properties.Add(property); + } + + bool IPublishedContentExtended.HasAddedProperties + { + get { return _properties != null; } + } + + void IPublishedContentExtended.SetContentSet(IEnumerable contentSet) + { + _contentSet = contentSet; + } + + void IPublishedContentExtended.ClearContentSet() + { + _contentSet = null; + } + + void IPublishedContentExtended.SetIndex(int value) + { + _index = value; + } + + void IPublishedContentExtended.ClearIndex() + { + _index = null; + } + + #endregion + + #region Content set + + private IEnumerable _contentSet; + + public override IEnumerable ContentSet + { + get { return _contentSet ?? Content.ContentSet; } + } + + #endregion + + #region Properties + + private ICollection _properties; + + public override ICollection Properties + { + get + { + return _properties == null + ? Content.Properties + : Content.Properties.Union(_properties).ToList(); + } + } + + public override object this[string alias] + { + get + { + if (_properties != null) + { + var property = _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)); + if (property != null) return property.HasValue ? property.ObjectValue : null; + } + return Content[alias]; + } + } + + public override IPublishedProperty GetProperty(string alias) + { + return _properties == null + ? Content.GetProperty(alias) + : _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)) ?? Content.GetProperty(alias); + } + + #endregion + } + +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs new file mode 100644 index 0000000000..27c57ef3e9 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents a strongly-typed published content. + /// + /// Every strongly-typed published content class should inherit from PublishedContentModel + /// (or inherit from a class that inherits from... etc.) so they are picked by the factory. + public abstract class PublishedContentModel : PublishedContentExtended + { + /// + /// Initializes a new instance of the class with + /// an original instance. + /// + /// The original content. + protected PublishedContentModel(IPublishedContent content) + : base(content) + { } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelAttribute.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelAttribute.cs new file mode 100644 index 0000000000..8eaebf6dd1 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelAttribute.cs @@ -0,0 +1,29 @@ +using System; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Indicates that the class is a published content model for a specified content type. + /// + /// By default, the name of the class is assumed to be the content type alias. The + /// PublishedContentModelAttribute can be used to indicate a different alias. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class PublishedContentModelAttribute : Attribute + { + /// + /// Initializes a new instance of the class with a content type alias. + /// + /// The content type alias. + public PublishedContentModelAttribute(string contentTypeAlias) + { + if (string.IsNullOrWhiteSpace(contentTypeAlias)) + throw new ArgumentException("Argument cannot be null nor empty.", "contentTypeAlias"); + ContentTypeAlias = contentTypeAlias; + } + + /// + /// Gets or sets the content type alias. + /// + public string ContentTypeAlias { get; private set; } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs new file mode 100644 index 0000000000..ce63a640e6 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactory.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides strongly typed published content models services. + /// + internal static class PublishedContentModelFactory + { + /// + /// Creates a strongly typed published content model for an internal published content. + /// + /// The internal published content. + /// The strongly typed published content model. + public static IPublishedContent CreateModel(IPublishedContent content) + { + return PublishedContentModelFactoryResolver.Current.HasValue + ? PublishedContentModelFactoryResolver.Current.Factory.CreateModel(content) + : content; + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs new file mode 100644 index 0000000000..af2bdd6859 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryImpl.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Implements a strongly typed content model factory + /// + internal class PublishedContentModelFactoryImpl : IPublishedContentModelFactory + { + //private readonly Dictionary _constructors + // = new Dictionary(); + + private readonly Dictionary> _constructors + = new Dictionary>(); + + public PublishedContentModelFactoryImpl() + { + var types = PluginManager.Current.ResolveTypes(); + var ctorArgTypes = new[] { typeof(IPublishedContent) }; + + foreach (var type in types) + { + if (type.Inherits() == false) + throw new InvalidOperationException(string.Format("Type {0} is marked with PublishedContentModel attribute but does not inherit from PublishedContentExtended.", type.FullName)); + var constructor = type.GetConstructor(ctorArgTypes); + if (constructor == null) + throw new InvalidOperationException(string.Format("Type {0} is missing a public constructor with one argument of type IPublishedContent.", type.FullName)); + var attribute = type.GetCustomAttribute(false); + var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; + typeName = typeName.ToLowerInvariant(); + + if (_constructors.ContainsKey(typeName)) + throw new InvalidOperationException(string.Format("More that one type want to be a model for content type {0}.", typeName)); + + // should work everywhere, but slow + //_constructors[typeName] = constructor; + + // much faster with a dynamic method but potential MediumTrust issues + // here http://stackoverflow.com/questions/16363838/how-do-you-call-a-constructor-via-an-expression-tree-on-an-existing-object + + // fast enough and works in MediumTrust + // read http://boxbinary.com/2011/10/how-to-run-a-unit-test-in-medium-trust-with-nunitpart-three-umbraco-framework-testing/ + var exprArg = Expression.Parameter(typeof(IPublishedContent), "content"); + var exprNew = Expression.New(constructor, exprArg); + var expr = Expression.Lambda>(exprNew, exprArg); + var func = expr.Compile(); + _constructors[typeName] = func; + } + } + + public IPublishedContent CreateModel(IPublishedContent content) + { + // be case-insensitive + var contentTypeAlias = content.DocumentTypeAlias.ToLowerInvariant(); + + //ConstructorInfo constructor; + //return _constructors.TryGetValue(contentTypeAlias, out constructor) + // ? (IPublishedContent) constructor.Invoke(new object[] { content }) + // : content; + + Func constructor; + return _constructors.TryGetValue(contentTypeAlias, out constructor) + ? constructor(content) + : content; + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryResolver.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryResolver.cs new file mode 100644 index 0000000000..6995cafc7f --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentModelFactoryResolver.cs @@ -0,0 +1,45 @@ +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Resolves the IPublishedContentModelFactory object. + /// + internal class PublishedContentModelFactoryResolver : SingleObjectResolverBase + { + /// + /// Initializes a new instance of the . + /// + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal PublishedContentModelFactoryResolver() + : base() + { } + + /// + /// Initializes a new instance of the with a factory. + /// + /// The factory. + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal PublishedContentModelFactoryResolver(IPublishedContentModelFactory factory) + : base(factory) + { } + + /// + /// Sets the factory. + /// + /// The factory. + /// For developers, at application startup. + public void SetFactory(IPublishedContentModelFactory factory) + { + Value = factory; + } + + /// + /// Gets the factory. + /// + public IPublishedContentModelFactory Factory + { + get { return Value; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentOrderedSet.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentOrderedSet.cs new file mode 100644 index 0000000000..ffb67876e7 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentOrderedSet.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents an ordered set of . + /// + /// The type of content. + public class PublishedContentOrderedSet : PublishedContentSet, IOrderedEnumerable + where T : class, IPublishedContent + { +// ReSharper disable ParameterTypeCanBeEnumerable.Local + internal PublishedContentOrderedSet(IOrderedEnumerable content) +// ReSharper restore ParameterTypeCanBeEnumerable.Local + : base(content) + { } + + // note: because we implement IOrderedEnumerable, we don't need to implement the ThenBy nor + // ThenByDescending methods here, only CreateOrderedEnumerable and that does it. + + #region IOrderedEnumerable + + public IOrderedEnumerable CreateOrderedEnumerable(Func keySelector, IComparer comparer, bool descending) + { + return new PublishedContentOrderedSet(((IOrderedEnumerable)Source).CreateOrderedEnumerable(keySelector, comparer, descending)); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentSet.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentSet.cs new file mode 100644 index 0000000000..bf4e983c7c --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentSet.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents a set of . + /// + /// The type of content. + /// + /// A ContentSet{T} is created from an IEnumerable{T} using the ToContentSet + /// extension method. + /// The content set source is enumerated only once. Same as what you get + /// when you call ToList on an IEnumerable. Only, ToList enumerates its source when + /// created, whereas a content set enumerates its source only when the content set itself + /// is enumerated. + /// + public class PublishedContentSet : IEnumerable + where T : class, IPublishedContent + { + // used by ToContentSet extension method to initialize a new set from an IEnumerable. + internal PublishedContentSet(IEnumerable source) + { + if (source == null) + throw new ArgumentNullException("source"); + Source = source; + } + + #region Source + + protected readonly IEnumerable Source; + + #endregion + + #region Enumerated + + // cache the enumeration so we don't enumerate more than once. Same as what you get + // when you call ToList on an IEnumerable. Only, ToList enumerates its source when + // created, whereas a content set enumerates its source only when the content set itself + // is enumerated. + + // cache the wrapped items so if we reset the enumeration, we do not re-wrap everything (only new items). + + private T[] _enumerated; + private readonly Dictionary _xContent = new Dictionary(); + + // wrap an item, ie create the actual clone for this set + private T MapContentAsT(T t) + { + return MapContent(t) as T; + } + + internal IPublishedContentExtended MapContent(T t) + { + IPublishedContentExtended extend; + if (_xContent.TryGetValue(t, out extend)) return extend; + + extend = PublishedContentExtended.Extend(t, this); + var asT = extend as T; + if (asT == null) + throw new InvalidOperationException(string.Format("Failed extend a published content of type {0}." + + "Got {1} when expecting {2}.", t.GetType().FullName, extend.GetType().FullName, typeof(T).FullName)); + _xContent[t] = extend; + return extend; + } + + private T[] Enumerated + { + get + { + // enumerate the source and cache the result + // tell clones about their index within the set (for perfs purposes) + var index = 0; + return _enumerated ?? (_enumerated = Source.Select(t => + { + var extend = MapContent(t); + extend.SetIndex(index++); + return extend as T; + }).ToArray()); + } + } + + // indicates that the source has changed + // so the set can clear its inner caches + // should only be used by DynamicPublishedContentList + internal void SourceChanged() + { + // reset the cached enumeration so it's enumerated again + if (_enumerated == null) return; + _enumerated = null; + + foreach (var item in _xContent.Values) + item.ClearIndex(); + + var removed = _xContent.Keys.Except(Source); + foreach (var content in removed) + { + _xContent[content].ClearContentSet(); + _xContent.Remove(content); + } + } + + /// + /// Gets the number of items in the set. + /// + /// The number of items in the set. + /// Will cause the set to be enumerated if it hasn't been already. + public virtual int Count + { + get { return Enumerated.Length; } + } + #endregion + + #region IEnumerable + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)Enumerated).GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Wrap methods returning T + + public T ElementAt(int index) + { + return MapContentAsT(Source.ElementAt(index)); + } + + public T ElementAtOrDefault(int index) + { + var element = Source.ElementAtOrDefault(index); + return element == null ? null : MapContentAsT(element); + } + + public T First() + { + return MapContentAsT(Source.First()); + } + + public T First(Func predicate) + { + return MapContentAsT(Source.First(predicate)); + } + + public T FirstOrDefault() + { + var first = Source.FirstOrDefault(); + return first == null ? null : MapContentAsT(first); + } + + public T FirstOrDefault(Func predicate) + { + var first = Source.FirstOrDefault(predicate); + return first == null ? null : MapContentAsT(first); + } + + public T Last() + { + return MapContentAsT(Source.Last()); + } + + public T Last(Func predicate) + { + return MapContentAsT(Source.Last(predicate)); + } + + public T LastOrDefault() + { + var last = Source.LastOrDefault(); + return last == null ? null : MapContentAsT(last); + } + + public T LastOrDefault(Func predicate) + { + var last = Source.LastOrDefault(predicate); + return last == null ? null : MapContentAsT(last); + } + + public T Single() + { + return MapContentAsT(Source.Single()); + } + + public T Single(Func predicate) + { + return MapContentAsT(Source.Single(predicate)); + } + + public T SingleOrDefault() + { + var single = Source.SingleOrDefault(); + return single == null ? null : MapContentAsT(single); + } + + public T SingleOrDefault(Func predicate) + { + var single = Source.SingleOrDefault(predicate); + return single == null ? null : MapContentAsT(single); + } + + #endregion + + #region Wrap methods returning IOrderedEnumerable + + public PublishedContentOrderedSet OrderBy(Func keySelector) + { + return new PublishedContentOrderedSet(Source.OrderBy(keySelector)); + } + + public PublishedContentOrderedSet OrderBy(Func keySelector, IComparer comparer) + { + return new PublishedContentOrderedSet(Source.OrderBy(keySelector, comparer)); + } + + public PublishedContentOrderedSet OrderByDescending(Func keySelector) + { + return new PublishedContentOrderedSet(Source.OrderByDescending(keySelector)); + } + + public PublishedContentOrderedSet OrderByDescending(Func keySelector, IComparer comparer) + { + return new PublishedContentOrderedSet(Source.OrderByDescending(keySelector, comparer)); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs new file mode 100644 index 0000000000..773da45ccf --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Web.Caching; +using System.Web.UI; +using Umbraco.Core.Cache; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents an type. + /// + /// Instances of the class are immutable, ie + /// if the content type changes, then a new class needs to be created. + public class PublishedContentType + { + private readonly PublishedPropertyType[] _propertyTypes; + + // fast alias-to-index xref containing both the raw alias and its lowercase version + private readonly Dictionary _indexes = new Dictionary(); + + // internal so it can be used by PublishedNoCache which does _not_ want to cache anything and so will never + // use the static cache getter PublishedContentType.GetPublishedContentType(alias) below - anything else + // should use it. + internal PublishedContentType(IContentTypeComposition contentType) + { + Id = contentType.Id; + Alias = contentType.Alias; + _propertyTypes = contentType.CompositionPropertyTypes + .Select(x => new PublishedPropertyType(this, x)) + .ToArray(); + InitializeIndexes(); + } + + // internal so it can be used for unit tests + internal PublishedContentType(int id, string alias, IEnumerable propertyTypes) + { + Id = id; + Alias = alias; + _propertyTypes = propertyTypes.ToArray(); + foreach (var propertyType in _propertyTypes) + propertyType.ContentType = this; + InitializeIndexes(); + } + + private void InitializeIndexes() + { + for (var i = 0; i < _propertyTypes.Length; i++) + { + var propertyType = _propertyTypes[i]; + _indexes[propertyType.PropertyTypeAlias] = i; + _indexes[propertyType.PropertyTypeAlias.ToLowerInvariant()] = i; + } + } + + #region Content type + + public int Id { get; private set; } + public string Alias { get; private set; } + + #endregion + + #region Properties + + public IEnumerable PropertyTypes + { + get { return _propertyTypes; } + } + + // alias is case-insensitive + // this is the ONLY place where we compare ALIASES! + public int GetPropertyIndex(string alias) + { + int index; + if (_indexes.TryGetValue(alias, out index)) return index; // fastest + if (_indexes.TryGetValue(alias.ToLowerInvariant(), out index)) return index; // slower + return -1; + } + + // virtual for unit tests + public virtual PublishedPropertyType GetPropertyType(string alias) + { + var index = GetPropertyIndex(alias); + return GetPropertyType(index); + } + + // virtual for unit tests + public virtual PublishedPropertyType GetPropertyType(int index) + { + return index >= 0 && index < _propertyTypes.Length ? _propertyTypes[index] : null; + } + + #endregion + + #region Cache + + // these methods are called by ContentTypeCacheRefresher and DataTypeCacheRefresher + + internal static void ClearAll() + { + Logging.LogHelper.Debug("Clear all."); + // ok and faster to do it by types, assuming noone else caches PublishedContentType instances + //ApplicationContext.Current.ApplicationCache.ClearStaticCacheByKeySearch("PublishedContentType_"); + ApplicationContext.Current.ApplicationCache.ClearStaticCacheObjectTypes(); + } + + internal static void ClearContentType(int id) + { + Logging.LogHelper.Debug("Clear content type w/id {0}.", () => id); + // requires a predicate because the key does not contain the ID + // faster than key strings comparisons anyway + ApplicationContext.Current.ApplicationCache.ClearStaticCacheObjectTypes( + (key, value) => value.Id == id); + } + + internal static void ClearDataType(int id) + { + Logging.LogHelper.Debug("Clear data type w/id {0}.", () => id); + // there is no recursion to handle here because a PublishedContentType contains *all* its + // properties ie both its own properties and those that were inherited (it's based upon an + // IContentTypeComposition) and so every PublishedContentType having a property based upon + // the cleared data type, be it local or inherited, will be cleared. + ApplicationContext.Current.ApplicationCache.ClearStaticCacheObjectTypes( + (key, value) => value.PropertyTypes.Any(x => x.DataTypeId == id)); + } + + public static PublishedContentType Get(PublishedItemType itemType, string alias) + { + var key = string.Format("PublishedContentType_{0}_{1}", + itemType == PublishedItemType.Content ? "content" : "media", alias.ToLowerInvariant()); + + var type = ApplicationContext.Current.ApplicationCache.GetStaticCacheItem(key, + () => CreatePublishedContentType(itemType, alias)); + + return type; + } + + private static PublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) + { + if (GetPublishedContentTypeCallback != null) + return GetPublishedContentTypeCallback(alias); + + var contentType = itemType == PublishedItemType.Content + ? (IContentTypeComposition) ApplicationContext.Current.Services.ContentTypeService.GetContentType(alias) + : (IContentTypeComposition) ApplicationContext.Current.Services.ContentTypeService.GetMediaType(alias); + + return new PublishedContentType(contentType); + } + + // for unit tests - changing the callback must reset the cache obviously + private static Func _getPublishedContentTypeCallBack; + internal static Func GetPublishedContentTypeCallback + { + get { return _getPublishedContentTypeCallBack; } + set + { + // see note above + //ClearAll(); + ApplicationContext.Current.ApplicationCache.ClearStaticCacheByKeySearch("PublishedContentType_"); + + _getPublishedContentTypeCallBack = value; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs new file mode 100644 index 0000000000..38e2537b40 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.PublishedContent +{ + // + // This class has two purposes. + // + // - First, we cannot implement strongly-typed content by inheriting from some sort + // of "master content" because that master content depends on the actual content cache + // that is being used. It can be an XmlPublishedContent with the XmlPublishedCache, + // or just anything else. + // + // So we implement strongly-typed content by encapsulating whatever content is + // returned by the content cache, and providing extra properties (mostly) or + // methods or whatever. This class provides the base for such encapsulation. + // + // - Second, any time a content is used in a content set obtained from + // IEnumerable.ToContentSet(), it needs to be cloned and extended + // in order to know about its position in the set. This class provides the base + // for implementing such extension. + // + + /// + /// Provides an abstract base class for IPublishedContent implementations that + /// wrap and extend another IPublishedContent. + /// + public abstract class PublishedContentWrapped : IPublishedContent + { + protected readonly IPublishedContent Content; + + /// + /// Initialize a new instance of the class + /// with an IPublishedContent instance to wrap and extend. + /// + /// The content to wrap and extend. + protected PublishedContentWrapped(IPublishedContent content) + { + Content = content; + } + + /// + /// Gets the wrapped content. + /// + /// The wrapped content, that was passed as an argument to the constructor. + public IPublishedContent Unwrap() + { + return Content; + } + + #region ContentSet + + public virtual IEnumerable ContentSet + { + get { return Content.ContentSet; } + } + + #endregion + + #region ContentType + + public virtual PublishedContentType ContentType { get { return Content.ContentType; } } + + #endregion + + #region Content + + public virtual int Id + { + get { return Content.Id; } + } + + public virtual int TemplateId + { + get { return Content.TemplateId; } + } + + public virtual int SortOrder + { + get { return Content.SortOrder; } + } + + public virtual string Name + { + get { return Content.Name; } + } + + public virtual string UrlName + { + get { return Content.UrlName; } + } + + public virtual string DocumentTypeAlias + { + get { return Content.DocumentTypeAlias; } + } + + public virtual int DocumentTypeId + { + get { return Content.DocumentTypeId; } + } + + public virtual string WriterName + { + get { return Content.WriterName; } + } + + public virtual string CreatorName + { + get { return Content.CreatorName; } + } + + public virtual int WriterId + { + get { return Content.WriterId; } + } + + public virtual int CreatorId + { + get { return Content.CreatorId; } + } + + public virtual string Path + { + get { return Content.Path; } + } + + public virtual DateTime CreateDate + { + get { return Content.CreateDate; } + } + + public virtual DateTime UpdateDate + { + get { return Content.UpdateDate; } + } + + public virtual Guid Version + { + get { return Content.Version; } + } + + public virtual int Level + { + get { return Content.Level; } + } + + public virtual string Url + { + get { return Content.Url; } + } + + public virtual PublishedItemType ItemType + { + get { return Content.ItemType; } + } + + public virtual bool IsDraft + { + get { return Content.IsDraft; } + } + + public virtual int GetIndex() + { + return Content.GetIndex(); + } + + #endregion + + #region Tree + + public virtual IPublishedContent Parent + { + get { return Content.Parent; } + } + + public virtual IEnumerable Children + { + get { return Content.Children; } + } + + #endregion + + #region Properties + + public virtual ICollection Properties + { + get { return Content.Properties; } + } + + public virtual object this[string alias] + { + get { return Content[alias]; } + } + + public virtual IPublishedProperty GetProperty(string alias) + { + return Content.GetProperty(alias); + } + + public virtual IPublishedProperty GetProperty(string alias, bool recurse) + { + return Content.GetProperty(alias, recurse); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs new file mode 100644 index 0000000000..d19a79b149 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -0,0 +1,31 @@ +using System; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Provides a base class for IPublishedProperty implementations which converts and caches + /// the value source to the actual value to use when rendering content. + /// + internal abstract class PublishedPropertyBase : IPublishedProperty + { + public readonly PublishedPropertyType PropertyType; + + protected PublishedPropertyBase(PublishedPropertyType propertyType) + { + if (propertyType == null) + throw new ArgumentNullException("propertyType"); + PropertyType = propertyType; + } + + public string PropertyTypeAlias + { + get { return PropertyType.PropertyTypeAlias; } + } + + // these have to be provided by the actual implementation + public abstract bool HasValue { get; } + public abstract object DataValue { get; } + public abstract object ObjectValue { get; } + public abstract object XPathValue { get; } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs new file mode 100644 index 0000000000..554b6bcfa8 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; +using Umbraco.Core.Dynamics; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Models.PublishedContent +{ + /// + /// Represents an type. + /// + /// Instances of the class are immutable, ie + /// if the property type changes, then a new class needs to be created. + public class PublishedPropertyType + { + public PublishedPropertyType(PublishedContentType contentType, PropertyType propertyType) + { + // PropertyEditor [1:n] DataTypeDefinition [1:n] PropertyType + + ContentType = contentType; + PropertyTypeAlias = propertyType.Alias; + + DataTypeId = propertyType.DataTypeDefinitionId; + PropertyEditorGuid = propertyType.DataTypeId; + //PropertyEditorAlias = propertyType.PropertyEditorAlias; + + InitializeConverters(); + } + + // for unit tests + internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, Guid propertyEditorGuid) + //internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, Alias propertyEditorAlias) + { + // ContentType to be set by PublishedContentType when creating it + PropertyTypeAlias = propertyTypeAlias; + + DataTypeId = dataTypeDefinitionId; + PropertyEditorGuid = propertyEditorGuid; + //PropertyEditorAlias = PropertyEditorAlias; + + InitializeConverters(); + } + + #region Property type + + /// + /// Gets or sets the published content type containing the property type. + /// + // internally set by PublishedContentType constructor + public PublishedContentType ContentType { get; internal set; } + + /// + /// Gets or sets the alias uniquely identifying the property type. + /// + public string PropertyTypeAlias { get; private set; } + + /// + /// Gets or sets the identifier uniquely identifying the data type supporting the property type. + /// + public int DataTypeId { get; private set; } + + /// + /// Gets or sets the guid uniquely identifying the property editor for the property type. + /// + public Guid PropertyEditorGuid { get; private set; } + + /// + /// Gets or sets the alias uniquely identifying the property editor for the property type. + /// + //public string PropertyEditorAlias { get; private set; } + + #endregion + + #region Converters + + private IPropertyValueConverter _converter; + + private PropertyCacheLevel _sourceCacheLevel; + private PropertyCacheLevel _objectCacheLevel; + private PropertyCacheLevel _xpathCacheLevel; + + private void InitializeConverters() + { + var converters = PropertyValueConvertersResolver.Current.Converters.ToArray(); + + // todo: remove Union() once we drop IPropertyEditorValueConverter support. + _converter = null; + foreach (var converter in converters.Union(GetCompatConverters()).Where(x => x.IsConverter(this))) + { + if (_converter == null) + { + _converter = converter; + } + else + { + throw new InvalidOperationException(string.Format("More than one converter for property type {0}.{1}", + ContentType.Alias, PropertyTypeAlias)); + } + } + + // get the cache levels, quietely fixing the inconsistencies (no need to throw, really) + _sourceCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Source); + _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Object); + _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); + if (_objectCacheLevel < _sourceCacheLevel) _objectCacheLevel = _sourceCacheLevel; + if (_xpathCacheLevel < _sourceCacheLevel) _xpathCacheLevel = _sourceCacheLevel; + } + + static PropertyCacheLevel GetCacheLevel(IPropertyValueConverter converter, PropertyCacheValue value) + { + if (converter == null) + return PropertyCacheLevel.Request; + + var attr = converter.GetType().GetCustomAttributes(false) + .FirstOrDefault(x => x.Value == value || x.Value == PropertyCacheValue.All); + + return attr == null ? PropertyCacheLevel.Request : attr.Level; + } + + // converts the raw value into the source value + // uses converters, else falls back to dark (& performance-wise expensive) magic + // source: the property raw value + // preview: whether we are previewing or not + public object ConvertDataToSource(object source, bool preview) + { + // use the converter else use dark (& performance-wise expensive) magic + return _converter != null + ? _converter.ConvertDataToSource(this, source, preview) + : ConvertUsingDarkMagic(source); + } + + // gets the source cache level + public PropertyCacheLevel SourceCacheLevel { get { return _sourceCacheLevel; } } + + // converts the source value into the clr value + // uses converters, else returns the source value + // source: the property source value + // preview: whether we are previewing or not + public object ConvertSourceToObject(object source, bool preview) + { + // use the converter if any + // else just return the source value + return _converter != null + ? _converter.ConvertSourceToObject(this, source, preview) + : source; + } + + // gets the value cache level + public PropertyCacheLevel ObjectCacheLevel { get { return _objectCacheLevel; } } + + // converts the source value into the xpath value + // uses the converter else returns the source value as a string + // if successful, returns either a string or an XPathNavigator + // source: the property source value + // preview: whether we are previewing or not + public object ConvertSourceToXPath(object source, bool preview) + { + // use the converter if any + if (_converter != null) + return _converter.ConvertSourceToXPath(this, source, preview); + + // else just return the source value as a string or an XPathNavigator + if (source == null) return null; + var xElement = source as XElement; + if (xElement != null) + return xElement.CreateNavigator(); + return source.ToString().Trim(); + } + + // gets the xpath cache level + public PropertyCacheLevel XPathCacheLevel { get { return _xpathCacheLevel; } } + + internal static object ConvertUsingDarkMagic(object source) + { + // convert to string + var stringSource = source as string; + if (stringSource == null) return source; // not a string => return the object + stringSource = stringSource.Trim(); + if (stringSource.Length == 0) return null; // empty string => return null + + // try numbers and booleans + // make sure we use the invariant culture ie a dot decimal point, comma is for csv + // NOTE far from perfect: "01a" is returned as a string but "012" is returned as an integer... + int i; + if (int.TryParse(stringSource, NumberStyles.Integer, CultureInfo.InvariantCulture, out i)) + return i; + float f; + if (float.TryParse(stringSource, NumberStyles.Float, CultureInfo.InvariantCulture, out f)) + return f; + bool b; + if (bool.TryParse(stringSource, out b)) + return b; + + // try xml - that is expensive, performance-wise + XElement elt; + if (XmlHelper.TryCreateXElementFromPropertyValue(stringSource, out elt)) + return new DynamicXml(elt); // xml => return DynamicXml for compatiblity's sake + + return source; + } + + #endregion + + #region Compat + + // backward-compatibility: support IPropertyEditorValueConverter while we have to + // todo: remove once we drop IPropertyEditorValueConverter support. + + IEnumerable GetCompatConverters() + { + return PropertyEditorValueConvertersResolver.HasCurrent + ? PropertyEditorValueConvertersResolver.Current.Converters + .Where(x => x.IsConverterFor(PropertyEditorGuid, ContentType.Alias, PropertyTypeAlias)) + .Select(x => new CompatConverter(x)) + : Enumerable.Empty(); + } + + class CompatConverter : PropertyValueConverterBase + { + private readonly IPropertyEditorValueConverter _converter; + + public CompatConverter(IPropertyEditorValueConverter converter) + { + _converter = converter; + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return true; + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + // NOTE: ignore preview, because IPropertyEditorValueConverter does not support it + return _converter.ConvertPropertyValue(source).Result; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Models/PublishedItemType.cs b/src/Umbraco.Core/Models/PublishedItemType.cs index e6e7a8f5bd..fd0f218cce 100644 --- a/src/Umbraco.Core/Models/PublishedItemType.cs +++ b/src/Umbraco.Core/Models/PublishedItemType.cs @@ -1,11 +1,18 @@ namespace Umbraco.Core.Models { /// - /// The type of published item + /// The type of published content, ie whether it is a content or a media. /// public enum PublishedItemType { + /// + /// A content, ie what was formerly known as a document. + /// Content, + + /// + /// A media. + /// Media } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs index 4b50d835fe..29e4c472b7 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs @@ -65,12 +65,12 @@ namespace Umbraco.Core.Models.Rdbms return Date.Value; } - if(!string.IsNullOrEmpty(VarChar)) + if(string.IsNullOrEmpty(VarChar) == false) { return VarChar; } - if(!string.IsNullOrEmpty(Text)) + if(string.IsNullOrEmpty(Text) == false) { return Text; } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index e41f904771..2695f1ee03 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -179,7 +179,7 @@ namespace Umbraco.Core.Persistence.Repositories .From() .InnerJoin().On(left => left.VersionId, right => right.VersionId) .Where(x => x.VersionId == versionId) - .Where(x => x.Newest == true); + .Where(x => x.Newest != true); var dto = Database.Fetch(sql).FirstOrDefault(); if(dto == null) return; @@ -192,6 +192,29 @@ namespace Umbraco.Core.Persistence.Repositories } } + public override void DeleteVersions(int id, DateTime versionDate) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin().On(left => left.VersionId, right => right.VersionId) + .Where(x => x.NodeId == id) + .Where(x => x.VersionDate < versionDate) + .Where(x => x.Newest != true); + var list = Database.Fetch(sql); + if (list.Any() == false) return; + + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } + + transaction.Complete(); + } + } + protected override void PerformDeleteVersion(int id, Guid versionId) { Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 144e2a9d8f..aa754c765d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -61,7 +61,8 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); - return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)); + return ConvertFromDtos(Database.Fetch(new UserSectionRelator().Map, sql)) + .ToArray(); // important so we don't iterate twice, if we don't do thsi we can end up with null vals in cache if we were caching. } private IEnumerable PerformGetAllOnIds(params int[] ids) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 64e0e833f0..1b3ff63eea 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -46,6 +46,11 @@ namespace Umbraco.Core.Persistence.Repositories var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); if(dto == null) return; + //Ensure that the lastest version is not deleted + var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); + if(latestVersionDto.VersionId == dto.VersionId) + return; + using (var transaction = Database.GetTransaction()) { PerformDeleteVersion(dto.NodeId, versionId); @@ -56,7 +61,12 @@ namespace Umbraco.Core.Persistence.Repositories public virtual void DeleteVersions(int id, DateTime versionDate) { - var list = Database.Fetch("WHERE ContentId = @Id AND VersionDate < @VersionDate", new { Id = id, VersionDate = versionDate }); + //Ensure that the latest version is not part of the versions being deleted + var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = id }); + var list = + Database.Fetch( + "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", + new {VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate}); if (list.Any() == false) return; using (var transaction = Database.GetTransaction()) diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs index 53d44ec4e8..770ee65aed 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs @@ -1,32 +1,30 @@ using System; -using Umbraco.Core.Dynamics; namespace Umbraco.Core.PropertyEditors { - [Obsolete("Use PropertyConverter instead since we no longer reference property editors by a GUID and instead by a string alias")] - public interface IPropertyEditorValueConverter + /// Maps a property source value to a data object. + /// + // todo: drop IPropertyEditorValueConverter support (when?). + [Obsolete("Use IPropertyValueConverter.")] + public interface IPropertyEditorValueConverter { + /// + /// Returns a value indicating whether this provider applies to the specified property. + /// + /// A Guid identifying the property datatype. + /// The content type alias. + /// The property alias. + /// True if this provider applies to the specified property. + bool IsConverterFor(Guid datatypeGuid, string contentTypeAlias, string propertyTypeAlias); /// - /// Returns true if this converter can perform the value conversion for the specified property editor id + /// Attempts to convert a source value specified into a property model. /// - /// - /// - /// - /// - bool IsConverterFor(Guid propertyEditorId, string docTypeAlias, string propertyTypeAlias); - - /// - /// Attempts to convert the value specified into a useable value on the front-end - /// - /// - /// - /// - /// This is used to convert the value stored in the repository into a usable value on the front-end. - /// For example, if a 0 or 1 is stored for a boolean, we'd want to convert this to a real boolean. - /// - /// Also note that the value might not come in as a 0 or 1 but as a "0" or "1" - /// - Attempt ConvertPropertyValue(object value); + /// The source value. + /// An Attempt representing the result of the conversion. + /// The source value is dependent on the content cache. With the Xml content cache it + /// is always a string, but with other caches it may be an object (numeric, time...) matching + /// what is in the database. Be prepared. + Attempt ConvertPropertyValue(object sourceValue); } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs new file mode 100644 index 0000000000..41d7dc7f7e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IPropertyValueConverter.cs @@ -0,0 +1,68 @@ +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Provides published content properties conversion service. + /// + public interface IPropertyValueConverter + { + /// + /// Gets a value indicating whether the converter supports a property type. + /// + /// The property type. + /// A value indicating whether the converter supports a property type. + bool IsConverter(PublishedPropertyType propertyType); + + /// + /// Converts a property Data value to a Source value. + /// + /// The property type. + /// The data value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// The converter should know how to convert a null raw value, meaning that no + /// value has been assigned to the property. The source value can be null. + /// With the XML cache, raw values come from the XML cache and therefore are strings. + /// With objects caches, raw values would come from the database and therefore be either + /// ints, DateTimes, or strings. + /// The converter should be prepared to handle both situations. + /// When raw values are strings, the converter must handle empty strings, whitespace + /// strings, and xml-whitespace strings appropriately, ie it should know whether to preserve + /// whitespaces. + /// + object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview); + + /// + /// Converts a property Source value to an Object value. + /// + /// The property type. + /// The source value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// The converter should know how to convert a null source value, or any source value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview); + + /// + /// Converts a property Source value to an XPath value. + /// + /// The property type. + /// The source value. + /// A value indicating whether conversion should take place in preview mode. + /// The result of the conversion. + /// + /// The converter should know how to convert a null source value, or any source value + /// indicating that no value has been assigned to the property. It is up to the converter to determine + /// what to return in that case: either null, or the default value... + /// If successful, the result should be either null, a string, or an XPathNavigator + /// instance. Whether an xml-whitespace string should be returned as null or litterally, is + /// up to the converter. + /// The converter may want to return an XML fragment that represent a part of the content tree, + /// but should pay attention not to create infinite loops that would kill XPath and XSLT. + /// + object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs new file mode 100644 index 0000000000..1e365d1ac4 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs @@ -0,0 +1,33 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Specifies the acceptable level of cache for a property value. + /// + /// By default, Request is assumed. + public enum PropertyCacheLevel + { + /// + /// Indicates that the property value can be cached at the content level, ie it can be + /// cached until the content itself is modified. + /// + Content = 1, + + /// + /// Indicates that the property value can be cached at the content cache level, ie it can + /// be cached until any content in the cache is modified. + /// + ContentCache = 2, + + /// + /// Indicates that the property value can be cached at the request level, ie it can be + /// cached for the duration of the current request. + /// + Request = 3, + + /// + /// Indicates that the property value cannot be cached and has to be converted any time + /// it is requested. + /// + None = 4 + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheValue.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheValue.cs new file mode 100644 index 0000000000..c4f438fb5e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheValue.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Specifies the different types of property cacheable values. + /// + public enum PropertyCacheValue + { + /// + /// All of them. + /// + All, + + /// + /// The source value ie the internal value that can be used to create both the + /// object value and the xpath value. + /// + Source, + + /// + /// The object value ie the strongly typed value of the property as seen when accessing content via C#. + /// + Object, + + /// + /// The XPath value ie the value of the property as seen when accessing content via XPath. + /// + XPath + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs index 1882b1f1ef..f6c40a3323 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs @@ -4,17 +4,35 @@ using Umbraco.Core.ObjectResolution; namespace Umbraco.Core.PropertyEditors { - /// - /// Manages the list of legacy IPropertyEditorValueConverter's - /// - internal sealed class PropertyEditorValueConvertersResolver : ManyObjectsResolverBase + /// + /// Manages the list of IPropertyEditorValueConverter's + /// + internal sealed class PropertyEditorValueConvertersResolver : ManyObjectsResolverBase { - public PropertyEditorValueConvertersResolver(IEnumerable converters) + /// + /// Initializes a new instance of the class with + /// an initial list of converter types. + /// + /// The list of converter types + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal PropertyEditorValueConvertersResolver(IEnumerable converters) : base(converters) - { - } - - public IEnumerable Converters + { } + + /// + /// Initializes a new instance of the class with + /// an initial list of converter types. + /// + /// The list of converter types + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal PropertyEditorValueConvertersResolver(params Type[] converters) + : base(converters) + { } + + /// + /// Gets the converteres. + /// + public IEnumerable Converters { get { return Values; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueCacheAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueCacheAttribute.cs new file mode 100644 index 0000000000..76d16b79c6 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueCacheAttribute.cs @@ -0,0 +1,34 @@ +using System; +using log4net.Core; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Indicates the cache level for a property cacheable value. + /// + /// Use this attribute to mark property values converters. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public class PropertyValueCacheAttribute : Attribute + { + /// + /// Initializes a new instance of the class with a cacheable value and a cache level. + /// + /// The cacheable value. + /// The cache level. + public PropertyValueCacheAttribute(PropertyCacheValue value, PropertyCacheLevel level) + { + Value = value; + Level = level; + } + + /// + /// Gets or sets the cacheable value. + /// + public PropertyCacheValue Value { get; private set; } + + /// + /// Gets or sets the cache level; + /// + public PropertyCacheLevel Level { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs new file mode 100644 index 0000000000..cecba7d4a2 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -0,0 +1,30 @@ +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Provides a default overridable implementation for that does nothing. + /// + public class PropertyValueConverterBase : IPropertyValueConverter + { + public virtual bool IsConverter(PublishedPropertyType propertyType) + { + return false; + } + + public virtual object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + return PublishedPropertyType.ConvertUsingDarkMagic(source); + } + + public virtual object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + return source; + } + + public virtual object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + { + return source.ToString(); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConvertersResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConvertersResolver.cs index 5afc49bf17..af3b5cfcc7 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConvertersResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConvertersResolver.cs @@ -5,16 +5,34 @@ using Umbraco.Core.ObjectResolution; namespace Umbraco.Core.PropertyEditors { /// - /// Manages the list of PropertyValueConverter's + /// Resolves the IPropertyValueConverter objects. /// - internal sealed class PropertyValueConvertersResolver : ManyObjectsResolverBase - { - public PropertyValueConvertersResolver(IEnumerable converters) - : base(converters) - { - } + public sealed class PropertyValueConvertersResolver : ManyObjectsResolverBase + { + /// + /// Initializes a new instance of the class with + /// an initial list of converter types. + /// + /// The list of converter types + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal PropertyValueConvertersResolver(IEnumerable converters) + : base(converters) + { } - public IEnumerable Converters + /// + /// Initializes a new instance of the class with + /// an initial list of converter types. + /// + /// The list of converter types + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal PropertyValueConvertersResolver(params Type[] converters) + : base(converters) + { } + + /// + /// Gets the converters. + /// + public IEnumerable Converters { get { return Values; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueTypeAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueTypeAttribute.cs new file mode 100644 index 0000000000..5d41b7f184 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueTypeAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Indicates the CLR type of property object values returned by a converter. + /// + /// Use this attribute to mark property values converters. + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PropertyValueTypeAttribute : Attribute + { + /// + /// Initializes a new instance of the class with a type. + /// + /// The type. + public PropertyValueTypeAttribute(Type type) + { + Type = type; + } + + /// + /// Gets or sets the type. + /// + public Type Type { get; private set; } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs new file mode 100644 index 0000000000..bd7793aa0d --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Xml; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [PropertyValueType(typeof(DateTime))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class DatePickerValueConverter : PropertyValueConverterBase + { + private static readonly Guid[] DataTypeGuids = new[] + { + Guid.Parse(Constants.PropertyEditors.DateTime), + Guid.Parse(Constants.PropertyEditors.Date) + }; + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return DataTypeGuids.Contains(propertyType.PropertyEditorGuid); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return DateTime.MinValue; + + // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" + var sourceString = source as string; + if (sourceString != null) + { + DateTime value; + return DateTime.TryParseExact(sourceString, "yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out value) + ? value + : DateTime.MinValue; + } + + // in the database a DateTime is: DateTime + // default value is: DateTime.MinValue + return (source is DateTime) + ? source + : DateTime.MinValue; + } + + // default ConvertSourceToObject just returns source ie a DateTime value + + public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + { + // source should come from ConvertSource and be a DateTime already + return XmlConvert.ToString((DateTime) source, "yyyy-MM-ddTHH:mm:ss"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs new file mode 100644 index 0000000000..c7112e51d3 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TinyMceValueConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Web; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used. + /// + // PropertyCacheLevel.Content is ok here because that version of RTE converter does not parse {locallink} nor executes macros + [PropertyValueType(typeof(IHtmlString))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class TinyMceValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return Guid.Parse(Constants.PropertyEditors.TinyMCEv3).Equals(propertyType.PropertyEditorGuid); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + // in xml a string is: string + // in the database a string is: string + // default value is: null + return source; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + // source should come from ConvertSource and be a string (or null) already + return new HtmlString(source == null ? string.Empty : (string)source); + } + + public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + { + // source should come from ConvertSource and be a string (or null) already + return source; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs new file mode 100644 index 0000000000..b2f7cead56 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs @@ -0,0 +1,35 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [PropertyValueType(typeof(bool))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class YesNoValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return Guid.Parse(Constants.PropertyEditors.TrueFalse).Equals(propertyType.PropertyEditorGuid); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + // in xml a boolean is: string + // in the database a boolean is: string "1" or "0" or empty + // the converter does not need to handle anything else ("true"...) + + // default value is: false + var sourceString = source as string; + if (sourceString == null) return false; + return sourceString == "1"; + } + + // default ConvertSourceToObject just returns source ie a boolean value + + public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) + { + // source should come from ConvertSource and be a boolean already + return (bool) source ? "1" : "0"; + } + } +} diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs deleted file mode 100644 index 9888363f45..0000000000 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Net.Mime; -using System.Web; -using Umbraco.Core.Dynamics; -using Umbraco.Core.Models; -using umbraco.interfaces; - -namespace Umbraco.Core -{ - /// - /// Extension methods for IPublishedContent - /// - public static class PublishedContentExtensions - { - - #region GetProperty - public static IPublishedContentProperty GetProperty(this IPublishedContent content, string alias, bool recursive) - { - return content.GetPropertyRecursive(alias, recursive); - } - - private static IPublishedContentProperty GetPropertyRecursive(this IPublishedContent content, string alias, bool recursive = false) - { - if (!recursive) - { - return content.GetProperty(alias); - } - var context = content; - var prop = content.GetPropertyRecursive(alias); - while (prop == null || prop.Value == null || prop.Value.ToString().IsNullOrWhiteSpace()) - { - if (context.Parent == null) break; - context = context.Parent; - prop = context.GetPropertyRecursive(alias); - } - return prop; - } - #endregion - - #region HasValue - - public static bool HasValue(this IPublishedContentProperty prop) - { - if (prop == null) return false; - if (prop.Value == null) return false; - return !prop.Value.ToString().IsNullOrWhiteSpace(); - } - - public static bool HasValue(this IPublishedContent doc, string alias) - { - return doc.HasValue(alias, false); - } - public static bool HasValue(this IPublishedContent doc, string alias, bool recursive) - { - var prop = doc.GetProperty(alias, recursive); - if (prop == null) return false; - return prop.HasValue(); - } - public static IHtmlString HasValue(this IPublishedContent doc, string alias, string valueIfTrue, string valueIfFalse) - { - return doc.HasValue(alias, false) ? new HtmlString(valueIfTrue) : new HtmlString(valueIfFalse); - } - public static IHtmlString HasValue(this IPublishedContent doc, string alias, bool recursive, string valueIfTrue, string valueIfFalse) - { - return doc.HasValue(alias, recursive) ? new HtmlString(valueIfTrue) : new HtmlString(valueIfFalse); - } - public static IHtmlString HasValue(this IPublishedContent doc, string alias, string valueIfTrue) - { - return doc.HasValue(alias, false) ? new HtmlString(valueIfTrue) : new HtmlString(string.Empty); - } - public static IHtmlString HasValue(this IPublishedContent doc, string alias, bool recursive, string valueIfTrue) - { - return doc.HasValue(alias, recursive) ? new HtmlString(valueIfTrue) : new HtmlString(string.Empty); - } - #endregion - - /// - /// Returns the recursive value of a field by iterating up the parent chain but starting at the publishedContent passed in - /// - /// - /// - /// - public static string GetRecursiveValue(this IPublishedContent publishedContent, string fieldname) - { - //check for the cached value in the objects properties first - var cachedVal = publishedContent["__recursive__" + fieldname]; - if (cachedVal != null) - { - return cachedVal.ToString(); - } - - var contentValue = ""; - var currentContent = publishedContent; - - while (contentValue.IsNullOrWhiteSpace()) - { - var val = currentContent[fieldname]; - if (val == null || val.ToString().IsNullOrWhiteSpace()) - { - if (currentContent.Parent == null) - { - break; //we've reached the top - } - currentContent = currentContent.Parent; - } - else - { - contentValue = val.ToString(); //we've found a recursive val - } - } - - //cache this lookup in a new custom (hidden) property - publishedContent.Properties.Add(new PropertyResult("__recursive__" + fieldname, contentValue, PropertyResultType.CustomProperty)); - - return contentValue; - } - - public static bool IsVisible(this IPublishedContent doc) - { - var umbracoNaviHide = doc.GetProperty(Constants.Conventions.Content.NaviHide); - if (umbracoNaviHide != null) - { - return umbracoNaviHide.Value.ToString().Trim() != "1"; - } - return true; - } - - public static bool HasProperty(this IPublishedContent doc, string name) - { - if (doc != null) - { - var prop = doc.GetProperty(name); - - return (prop != null); - } - return false; - } - - - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/PublishedContentHelper.cs b/src/Umbraco.Core/PublishedContentHelper.cs deleted file mode 100644 index b663f3f904..0000000000 --- a/src/Umbraco.Core/PublishedContentHelper.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dynamics; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using log4net.Util.TypeConverters; - -namespace Umbraco.Core -{ - - /// - /// Utility class for dealing with data types and value conversions - /// - /// - /// TODO: The logic for the GetDataType + cache should probably be moved to a service, no ? - /// - /// We inherit from ApplicationEventHandler so we can bind to the ContentTypeService events to ensure that our local cache - /// object gets cleared when content types change. - /// - internal class PublishedContentHelper : ApplicationEventHandler - { - /// - /// Used to invalidate the cache from the ICacherefresher - /// - internal static void ClearPropertyTypeCache() - { - PropertyTypeCache.Clear(); - } - - /// - /// This callback is used only for unit tests which enables us to return any data we want and not rely on having the data in a database - /// - internal static Func GetDataTypeCallback = null; - - private static readonly ConcurrentDictionary, string> PropertyTypeCache = new ConcurrentDictionary, string>(); - - /// - /// Return the GUID Id for the data type assigned to the document type with the property alias - /// - /// - /// - /// - /// - /// - internal static string GetPropertyEditor(ApplicationContext applicationContext, string docTypeAlias, string propertyAlias, PublishedItemType itemType) - { - if (GetDataTypeCallback != null) - return GetDataTypeCallback(docTypeAlias, propertyAlias); - - var key = new Tuple(docTypeAlias, propertyAlias, itemType); - return PropertyTypeCache.GetOrAdd(key, tuple => - { - IContentTypeComposition result = null; - switch (itemType) - { - case PublishedItemType.Content: - result = applicationContext.Services.ContentTypeService.GetContentType(docTypeAlias); - break; - case PublishedItemType.Media: - result = applicationContext.Services.ContentTypeService.GetMediaType(docTypeAlias); - break; - default: - throw new ArgumentOutOfRangeException("itemType"); - } - - if (result == null) return string.Empty; - - //SD: we need to check for 'any' here because the collection is backed by KeyValuePair which is a struct - // and can never be null so FirstOrDefault doesn't actually work. Have told Seb and Morten about thsi - // issue. - if (!result.CompositionPropertyTypes.Any(x => x.Alias.InvariantEquals(propertyAlias))) - { - return string.Empty; - } - var property = result.CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyAlias)); - //as per above, this will never be null but we'll keep the check here anyways. - if (property == null) return string.Empty; - return property.PropertyEditorAlias; - }); - } - - /// - /// Converts the currentValue to a correctly typed value based on known registered converters, then based on known standards. - /// - /// - /// - /// - internal static Attempt ConvertPropertyValue(object currentValue, PublishedPropertyDefinition propertyDefinition) - { - if (currentValue == null) return Attempt.Fail(); - - //First, we need to check the v7+ PropertyValueConverters - var converters = PropertyValueConvertersResolver.Current.Converters - .Where(x => x.AssociatedPropertyEditorAlias == propertyDefinition.PropertyEditorAlias) - .ToArray(); - if (converters.Any()) - { - if (converters.Count() > 1) - { - throw new NotSupportedException("Only one " + typeof(PropertyValueConverter) + " can be registered for the property editor: " + propertyDefinition.PropertyEditorAlias); - } - var result = converters.Single().ConvertSourceToObject( - currentValue, - propertyDefinition, - false); - - //if it is good return it, otherwise we'll continue processing the legacy stuff below. - if (result.Success) - { - return new Attempt(true, result.Result); - } - } - - //In order to maintain backwards compatibility here with IPropertyEditorValueConverter we need to attempt to lookup the - // legacy GUID for the current property editor. If one doesn't exist then we will abort the conversion. - var legacyId = LegacyPropertyEditorIdToAliasConverter.GetLegacyIdFromAlias(propertyDefinition.PropertyEditorAlias); - if (legacyId.HasValue == false) - { - return Attempt.False; - } - - //First lets check all registered converters for this data type. - var legacyConverters = PropertyEditorValueConvertersResolver.Current.Converters - .Where(x => x.IsConverterFor(legacyId.Value, propertyDefinition.DocumentTypeAlias, propertyDefinition.PropertyTypeAlias)) - .ToArray(); - - //try to convert the value with any of the converters: - foreach (var converted in legacyConverters - .Select(p => p.ConvertPropertyValue(currentValue)) - .Where(converted => converted.Success)) - { - return Attempt.Succeed(converted.Result); - } - - //if none of the converters worked, then we'll process this from what we know - - var sResult = Convert.ToString(currentValue).Trim(); - - //this will eat csv strings, so only do it if the decimal also includes a decimal seperator (according to the current culture) - if (sResult.Contains(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator)) - { - decimal dResult; - if (decimal.TryParse(sResult, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.CurrentCulture, out dResult)) - { - return Attempt.Succeed(dResult); - } - } - //process string booleans as booleans - if (sResult.InvariantEquals("true")) - { - return Attempt.Succeed(true); - } - if (sResult.InvariantEquals("false")) - { - return Attempt.Succeed(false); - } - - //a really rough check to see if this may be valid xml - //TODO: This is legacy code, I'm sure there's a better and nicer way - if (sResult.StartsWith("<") && sResult.EndsWith(">") && sResult.Contains("/")) - { - try - { - var e = XElement.Parse(sResult, LoadOptions.None); - - //check that the document element is not one of the disallowed elements - //allows RTE to still return as html if it's valid xhtml - var documentElement = e.Name.LocalName; - - //TODO: See note against this setting, pretty sure we don't need this - if (UmbracoConfiguration.Current.UmbracoSettings.Scripting.NotDynamicXmlDocumentElements.Any( - tag => string.Equals(tag.Element, documentElement, StringComparison.CurrentCultureIgnoreCase)) == false) - { - return Attempt.Succeed(new DynamicXml(e)); - } - return Attempt.Fail(); - } - catch (Exception) - { - return Attempt.Fail(); - } - } - return Attempt.Fail(); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index fc8daac769..d4437a5000 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -830,15 +830,13 @@ namespace Umbraco.Core.Services /// /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. /// /// Id of the object to delete versions from /// Latest version date /// Optional Id of the User deleting versions of a Content object public void DeleteVersions(int id, DateTime versionDate, int userId = 0) { - //TODO: We should check if we are going to delete the most recent version because if that happens it means the - // entity is completely deleted and we should raise the normal Deleting/Deleted event - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) return; @@ -856,6 +854,7 @@ namespace Umbraco.Core.Services /// /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. /// /// Id of the object to delete a version from /// Id of the version to delete @@ -865,8 +864,8 @@ namespace Umbraco.Core.Services { using (new WriteLock(Locker)) { - //TODO: We should check if we are going to delete the most recent version because if that happens it means the - // entity is completely deleted and we should raise the normal Deleting/Deleted event + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; if (deletePriorVersions) { @@ -874,9 +873,6 @@ namespace Umbraco.Core.Services DeleteVersions(id, content.UpdateDate, userId); } - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index f3a967c096..f6a42aea9b 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -654,6 +654,7 @@ namespace Umbraco.Core.Services /// /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. /// /// Id of the object to delete versions from /// Latest version date @@ -677,6 +678,7 @@ namespace Umbraco.Core.Services /// /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. /// /// Id of the object to delete a version from /// Id of the version to delete @@ -684,15 +686,15 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; + if (deletePriorVersions) { var content = GetByVersion(versionId); DeleteVersions(id, content.UpdateDate, userId); } - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion:versionId), this)) - return; - var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMediaRepository(uow)) { diff --git a/src/Umbraco.Core/TypeExtensions.cs b/src/Umbraco.Core/TypeExtensions.cs index b7c079f384..c8da74f067 100644 --- a/src/Umbraco.Core/TypeExtensions.cs +++ b/src/Umbraco.Core/TypeExtensions.cs @@ -83,26 +83,35 @@ namespace Umbraco.Core return true; } - public static IEnumerable AllInterfaces(this Type target) + // that method is broken (will return duplicates) and useless (GetInterfaces already does the job) + //public static IEnumerable AllInterfaces(this Type target) + //{ + // foreach (var IF in target.GetInterfaces()) + // { + // yield return IF; + // foreach (var childIF in IF.AllInterfaces()) + // { + // yield return childIF; + // } + // } + //} + + public static IEnumerable GetBaseTypes(this Type type, bool andSelf) { - foreach (var IF in target.GetInterfaces()) - { - yield return IF; - foreach (var childIF in IF.AllInterfaces()) - { - yield return childIF; - } - } + if (andSelf) + yield return type; + + while ((type = type.BaseType) != null) + yield return type; } public static IEnumerable AllMethods(this Type target) { - var allTypes = target.AllInterfaces().ToList(); + //var allTypes = target.AllInterfaces().ToList(); + var allTypes = target.GetInterfaces().ToList(); // GetInterfaces is ok here allTypes.Add(target); - return from type in allTypes - from method in type.GetMethods() - select method; + return allTypes.SelectMany(t => t.GetMethods()); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e2f9310ac2..80e7d9d717 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -296,6 +296,22 @@ + + + + + + + + + + + + + + + + @@ -320,10 +336,16 @@ + + + + + + @@ -734,7 +756,6 @@ - @@ -818,7 +839,7 @@ - + @@ -826,7 +847,7 @@ - + @@ -847,14 +868,12 @@ - - - + @@ -985,6 +1004,12 @@ + + + + + + diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 2fda214e01..b935dce488 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -73,7 +73,8 @@ namespace Umbraco.Core /// internal static bool IsClientSideRequest(this Uri url) { - // fixme - but really, is this OK? we should accept either no url, or .aspx, and everything else is out + // fixme - IsClientSideRequest should not use an hard-coded list of extensions + // a client-side request is anything that has an extension that is not .aspx? var toIgnore = new[] { ".js", ".css", ".ico", ".png", ".jpg", ".jpeg", ".gif", ".html", ".svg" }; return toIgnore.Any(x => Path.GetExtension(url.LocalPath).InvariantEquals(x)); } diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs new file mode 100644 index 0000000000..d8b6adf00a --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents a content that can be navigated via XPath. + /// + interface INavigableContent + { + /// + /// Gets the unique identifier of the navigable content. + /// + /// The root node identifier should be -1. + int Id { get; } + + /// + /// Gets the unique identifier of parent of the navigable content. + /// + /// The top-level content parent identifiers should be -1 ie the identifier + /// of the root node, whose parent identifier should in turn be -1. + int ParentId { get; } + + /// + /// Gets the type of the navigable content. + /// + INavigableContentType Type { get; } + + /// + /// Gets the unique identifiers of the children of the navigable content. + /// + IList ChildIds { get; } + + /// + /// Gets the value of a field of the navigable content for XPath navigation use. + /// + /// The field index. + /// The value of the field for XPath navigation use. + /// + /// Fields are attributes or elements depending on their relative index value compared + /// to source.LastAttributeIndex. + /// For attributes, the value must be a string. + /// For elements, the value should an XPathNavigator instance if the field is xml + /// and has content (is not empty), null to indicate that the element is empty, or a string + /// which can be empty, whitespace... depending on what the data type wants to expose. + /// + object Value(int index); + + // TODO implement the following one + + ///// + ///// Gets the value of a field of the navigable content, for a specified language. + ///// + ///// The field index. + ///// The language key. + ///// The value of the field for the specified language. + ///// ... + //object Value(int index, string languageKey); + } +} diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs new file mode 100644 index 0000000000..94b225467c --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents the type of a content that can be navigated via XPath. + /// + interface INavigableContentType + { + /// + /// Gets the name of the content type. + /// + string Name { get; } + + /// + /// Gets the field types of the content type. + /// + /// This includes the attributes and the properties. + INavigableFieldType[] FieldTypes { get; } + } +} diff --git a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs new file mode 100644 index 0000000000..32a6f64751 --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents the type of a field of a content that can be navigated via XPath. + /// + /// A field can be an attribute or a property. + interface INavigableFieldType + { + /// + /// Gets the name of the field type. + /// + string Name { get; } + + /// + /// Gets a method to convert the field value to a string. + /// + /// This is for built-in properties, ie attributes. User-defined properties have their + /// own way to convert their value for XPath. + Func XmlStringConverter { get; } + } +} diff --git a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs new file mode 100644 index 0000000000..68dc9e906a --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents a source of content that can be navigated via XPath. + /// + interface INavigableSource + { + /// + /// Gets a content identified by its unique identifier. + /// + /// The unique identifier. + /// The content identified by the unique identifier, or null. + /// When id is -1 (root content) implementations should return null. + INavigableContent Get(int id); + + /// + /// Gets the index of the last attribute in the fields collections. + /// + int LastAttributeIndex { get; } + + /// + /// Gets the content at the root of the source. + /// + /// That content should have unique identifier -1 and should not be gettable, + /// ie Get(-1) should return null. Its ParentId should be -1. It should provide + /// values for the attribute fields. + INavigableContent Root { get; } + } +} diff --git a/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs new file mode 100644 index 0000000000..501c8c3d1a --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/MacroNavigator.cs @@ -0,0 +1,1042 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml; +using System.Xml.XPath; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Provides a cursor model for navigating {macro /} as if it were XML. + /// + class MacroNavigator : XPathNavigator + { + private readonly XmlNameTable _nameTable; + private readonly MacroRoot _macro; + private State _state; + + #region Constructor + + /// + /// Initializes a new instance of the class with macro parameters. + /// + /// The macro parameters. + public MacroNavigator(IEnumerable parameters) + : this(new MacroRoot(parameters), new NameTable(), new State()) + { } + + /// + /// Initializes a new instance of the class with a macro node, + /// a name table and a state. + /// + /// The macro node. + /// The name table. + /// The state. + /// Privately used for cloning a navigator. + private MacroNavigator(MacroRoot macro, XmlNameTable nameTable, State state) + { + _macro = macro; + _nameTable = nameTable; + _state = state; + } + + #endregion + + #region Diagnostics + + // diagnostics code will not be compiled nor called into Release configuration. + // in Debug configuration, uncomment lines in Debug() to write to console or to log. + // + // much of this code is duplicated in each navigator due to conditional compilation + +#if DEBUG + private const string Tabs = " "; + private int _tabs; + private readonly int _uid = GetUid(); + private static int _uidg; + private readonly static object Uidl = new object(); + private static int GetUid() + { + lock (Uidl) + { + return _uidg++; + } + } +#endif + + [Conditional("DEBUG")] + void DebugEnter(string name) + { +#if DEBUG + Debug(""); + DebugState(":"); + Debug(name); + _tabs = Math.Min(Tabs.Length, _tabs + 2); +#endif + } + + [Conditional("DEBUG")] + void DebugCreate(MacroNavigator nav) + { +#if DEBUG + Debug("Create: [MacroNavigator::{0}]", nav._uid); +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn() + { +#if DEBUG +// ReSharper disable IntroduceOptionalParameters.Local + DebugReturn("(void)"); +// ReSharper restore IntroduceOptionalParameters.Local +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn(bool value) + { +#if DEBUG + DebugReturn(value ? "true" : "false"); +#endif + } + + [Conditional("DEBUG")] + void DebugReturn(string format, params object[] args) + { +#if DEBUG + Debug("=> " + format, args); + if (_tabs > 0) _tabs -= 2; +#endif + } + + [Conditional("DEBUG")] + void DebugState(string s = " =>") + { +#if DEBUG + string position; + + switch (_state.Position) + { + case StatePosition.Macro: + position = "At macro."; + break; + case StatePosition.Parameter: + position = string.Format("At parameter '{0}'.", + _macro.Parameters[_state.ParameterIndex].Name); + break; + case StatePosition.ParameterAttribute: + position = string.Format("At parameter attribute '{0}/{1}'.", + _macro.Parameters[_state.ParameterIndex].Name, + _macro.Parameters[_state.ParameterIndex].Attributes[_state.ParameterAttributeIndex].Key); + break; + case StatePosition.ParameterNavigator: + position = string.Format("In parameter '{0}{1}' navigator.", + _macro.Parameters[_state.ParameterIndex].Name, + _macro.Parameters[_state.ParameterIndex].WrapNavigatorInNodes ? "/nodes" : ""); + break; + case StatePosition.ParameterNodes: + position = string.Format("At parameter '{0}/nodes'.", + _macro.Parameters[_state.ParameterIndex].Name); + break; + case StatePosition.ParameterText: + position = string.Format("In parameter '{0}' text.", + _macro.Parameters[_state.ParameterIndex].Name); + break; + case StatePosition.Root: + position = "At root."; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + Debug("State{0} {1}", s, position); +#endif + } + +#if DEBUG + void Debug(string format, params object[] args) + { + // remove comments to write + + format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format; +#pragma warning disable 168 + var msg = string.Format(format, args); // unused if not writing, hence #pragma +#pragma warning restore 168 + //LogHelper.Debug(msg); // beware! this can quicky overflow log4net + //Console.WriteLine(msg); + } +#endif + + #endregion + + #region Macro + + private class MacroRoot + { + public MacroRoot(IEnumerable parameters) + { + Parameters = parameters == null ? new MacroParameter[] {} : parameters.ToArray(); + } + + public MacroParameter[] Parameters { get; private set; } + } + + public class MacroParameter + { + // note: assuming we're not thinking about supporting + // XPathIterator in parameters - enough nonsense! + + public MacroParameter(string name, string value) + { + Name = name; + StringValue = value; + } + + public MacroParameter(string name, XPathNavigator navigator, + int maxNavigatorDepth = int.MaxValue, + bool wrapNavigatorInNodes = false, + IEnumerable> attributes = null) + { + Name = name; + MaxNavigatorDepth = maxNavigatorDepth; + WrapNavigatorInNodes = wrapNavigatorInNodes; + if (attributes != null) + { + var a = attributes.ToArray(); + if (a.Length > 0) + Attributes = a; + } + NavigatorValue = navigator; // should not be empty + } + + public string Name { get; private set; } + public string StringValue { get; private set; } + public XPathNavigator NavigatorValue { get; private set; } + public int MaxNavigatorDepth { get; private set; } + public bool WrapNavigatorInNodes { get; private set; } + public KeyValuePair[] Attributes { get; private set; } + } + + #endregion + + /// + /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. + /// + /// A new XPathNavigator positioned at the same node as this XPathNavigator. + public override XPathNavigator Clone() + { + DebugEnter("Clone"); + var nav = new MacroNavigator(_macro, _nameTable, _state.Clone()); + DebugCreate(nav); + DebugReturn("[XPathNavigator]"); + return nav; + } + + /// + /// Gets a value indicating whether the current node is an empty element without an end element tag. + /// + public override bool IsEmptyElement + { + get + { + DebugEnter("IsEmptyElement"); + bool isEmpty; + + switch (_state.Position) + { + case StatePosition.Macro: + isEmpty = _macro.Parameters.Length == 0; + break; + case StatePosition.Parameter: + var parameter = _macro.Parameters[_state.ParameterIndex]; + var nav = parameter.NavigatorValue; + if (parameter.WrapNavigatorInNodes || nav != null) + { + isEmpty = false; + } + else + { + var s = _macro.Parameters[_state.ParameterIndex].StringValue; + isEmpty = s == null; + } + break; + case StatePosition.ParameterNavigator: + isEmpty = _state.ParameterNavigator.IsEmptyElement; + break; + case StatePosition.ParameterNodes: + isEmpty = _macro.Parameters[_state.ParameterIndex].NavigatorValue == null; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterText: + case StatePosition.Root: + throw new InvalidOperationException("Not an element."); + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isEmpty); + return isEmpty; + } + } + + /// + /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. + /// + /// The XPathNavigator to compare to this XPathNavigator. + /// true if the two XPathNavigator objects have the same position; otherwise, false. + public override bool IsSamePosition(XPathNavigator nav) + { + DebugEnter("IsSamePosition"); + bool isSame; + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + case StatePosition.Macro: + case StatePosition.Parameter: + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.ParameterText: + case StatePosition.Root: + var other = nav as MacroNavigator; + isSame = other != null && other._macro == _macro && _state.IsSamePosition(other._state); + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isSame); + return isSame; + } + + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get + { + DebugEnter("Name"); + string name; + + switch (_state.Position) + { + case StatePosition.Macro: + name = "macro"; + break; + case StatePosition.Parameter: + name = _macro.Parameters[_state.ParameterIndex].Name; + break; + case StatePosition.ParameterAttribute: + name = _macro.Parameters[_state.ParameterIndex].Attributes[_state.ParameterAttributeIndex].Key; + break; + case StatePosition.ParameterNavigator: + name = _state.ParameterNavigator.Name; + break; + case StatePosition.ParameterNodes: + name = "nodes"; + break; + case StatePosition.ParameterText: + case StatePosition.Root: + name = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get + { + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Moves the XPathNavigator to the same position as the specified XPathNavigator. + /// + /// The XPathNavigator positioned on the node that you want to move to. + /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveTo(XPathNavigator nav) + { + DebugEnter("MoveTo"); + + var other = nav as MacroNavigator; + var succ = false; + + if (other != null && other._macro == _macro) + { + _state = other._state.Clone(); + DebugState(); + succ = true; + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first attribute of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstAttribute() + { + DebugEnter("MoveToFirstAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + succ = _state.ParameterNavigator.MoveToFirstAttribute(); + break; + case StatePosition.Parameter: + if (_macro.Parameters[_state.ParameterIndex].Attributes != null) + { + _state.Position = StatePosition.ParameterAttribute; + _state.ParameterAttributeIndex = 0; + succ = true; + DebugState(); + } + else succ = false; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.Macro: + case StatePosition.ParameterText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first child node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstChild() + { + DebugEnter("MoveToFirstChild"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Macro: + if (_macro.Parameters.Length == 0) + { + succ = false; + } + else + { + _state.ParameterIndex = 0; + _state.Position = StatePosition.Parameter; + succ = true; + } + break; + case StatePosition.Parameter: + var parameter = _macro.Parameters[_state.ParameterIndex]; + var nav = parameter.NavigatorValue; + if (parameter.WrapNavigatorInNodes) + { + _state.Position = StatePosition.ParameterNodes; + DebugState(); + succ = true; + } + else if (nav != null) + { + nav = nav.Clone(); // never use the raw parameter's navigator + nav.MoveToFirstChild(); + _state.ParameterNavigator = nav; + _state.ParameterNavigatorDepth = 0; + _state.Position = StatePosition.ParameterNavigator; + DebugState(); + succ = true; + } + else + { + var s = _macro.Parameters[_state.ParameterIndex].StringValue; + if (s != null) + { + _state.Position = StatePosition.ParameterText; + DebugState(); + succ = true; + } + else succ = false; + } + break; + case StatePosition.ParameterNavigator: + if (_state.ParameterNavigatorDepth == _macro.Parameters[_state.ParameterIndex].MaxNavigatorDepth) + { + succ = false; + } + else + { + // move to first doc child => increment depth, else (property child) do nothing + succ = _state.ParameterNavigator.MoveToFirstChild(); + if (succ && IsDoc(_state.ParameterNavigator)) + { + ++_state.ParameterNavigatorDepth; + DebugState(); + } + } + break; + case StatePosition.ParameterNodes: + if (_macro.Parameters[_state.ParameterIndex].NavigatorValue != null) + { + // never use the raw parameter's navigator + _state.ParameterNavigator = _macro.Parameters[_state.ParameterIndex].NavigatorValue.Clone(); + _state.Position = StatePosition.ParameterNavigator; + succ = true; + DebugState(); + } + else succ = false; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterText: + succ = false; + break; + case StatePosition.Root: + _state.Position = StatePosition.Macro; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the first namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToFirstNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the next namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToNextNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves to the node that has an attribute of type ID whose value matches the specified String. + /// + /// A String representing the ID value of the node to which you want to move. + /// true if the XPathNavigator is successful moving; otherwise, false. + /// If false, the position of the navigator is unchanged. + public override bool MoveToId(string id) + { + DebugEnter("MoveToId"); + // impossible to implement since parameters can contain duplicate fragments of the + // main xml and therefore there can be duplicate unique node identifiers. + DebugReturn("NotImplementedException"); + throw new NotImplementedException(); + } + + /// + /// Moves the XPathNavigator to the next sibling node of the current node. + /// + /// true if the XPathNavigator is successful moving to the next sibling node; + /// otherwise, false if there are no more siblings or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNext() + { + DebugEnter("MoveToNext"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Parameter: + if (_state.ParameterIndex == _macro.Parameters.Length - 1) + { + succ = false; + } + else + { + ++_state.ParameterIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.ParameterNavigator: + var wasDoc = IsDoc(_state.ParameterNavigator); + succ = _state.ParameterNavigator.MoveToNext(); + if (succ && !wasDoc && IsDoc(_state.ParameterNavigator)) + { + // move to first doc child => increment depth, else (another property child) do nothing + if (_state.ParameterNavigatorDepth == _macro.Parameters[_state.ParameterIndex].MaxNavigatorDepth) + { + _state.ParameterNavigator.MoveToPrevious(); + succ = false; + } + else + { + ++_state.ParameterNavigatorDepth; + DebugState(); + } + } + break; + case StatePosition.ParameterNodes: + case StatePosition.ParameterAttribute: + case StatePosition.ParameterText: + case StatePosition.Macro: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the previous sibling node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the previous sibling node; + /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToPrevious() + { + DebugEnter("MoveToPrevious"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Parameter: + if (_state.ParameterIndex == -1) + { + succ = false; + } + else + { + --_state.ParameterIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.ParameterNavigator: + var wasDoc = IsDoc(_state.ParameterNavigator); + succ = _state.ParameterNavigator.MoveToPrevious(); + if (succ && wasDoc && !IsDoc(_state.ParameterNavigator)) + { + // move from doc child back to property child => decrement depth + --_state.ParameterNavigatorDepth; + DebugState(); + } + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.ParameterText: + case StatePosition.Macro: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next attribute. + /// + /// Returns true if the XPathNavigator is successful moving to the next attribute; + /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextAttribute() + { + DebugEnter("MoveToNextAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + succ = _state.ParameterNavigator.MoveToNextAttribute(); + break; + case StatePosition.ParameterAttribute: + if (_state.ParameterAttributeIndex == _macro.Parameters[_state.ParameterIndex].Attributes.Length - 1) + succ = false; + else + { + ++_state.ParameterAttributeIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.Parameter: + case StatePosition.ParameterNodes: + case StatePosition.ParameterText: + case StatePosition.Macro: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the parent node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToParent() + { + DebugEnter("MoveToParent"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Macro: + _state.Position = StatePosition.Root; + DebugState(); + succ = true; + break; + case StatePosition.Parameter: + _state.Position = StatePosition.Macro; + DebugState(); + succ = true; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + _state.Position = StatePosition.Parameter; + DebugState(); + succ = true; + break; + case StatePosition.ParameterNavigator: + var wasDoc = IsDoc(_state.ParameterNavigator); + succ = _state.ParameterNavigator.MoveToParent(); + if (succ) + { + // move from doc child => decrement depth + if (wasDoc && --_state.ParameterNavigatorDepth == 0) + { + _state.Position = StatePosition.Parameter; + _state.ParameterNavigator = null; + DebugState(); + } + } + break; + case StatePosition.ParameterText: + _state.Position = StatePosition.Parameter; + DebugState(); + succ = true; + break; + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the root node that the current node belongs to. + /// + public override void MoveToRoot() + { + DebugEnter("MoveToRoot"); + + switch (_state.Position) + { + case StatePosition.ParameterNavigator: + _state.ParameterNavigator = null; + _state.ParameterNavigatorDepth = -1; + break; + case StatePosition.Parameter: + case StatePosition.ParameterText: + _state.ParameterIndex = -1; + break; + case StatePosition.ParameterAttribute: + case StatePosition.ParameterNodes: + case StatePosition.Macro: + case StatePosition.Root: + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + _state.Position = StatePosition.Root; + DebugState(); + + DebugReturn(); + } + + /// + /// Gets the base URI for the current node. + /// + public override string BaseURI + { + get { return string.Empty; } + } + + /// + /// Gets the XmlNameTable of the XPathNavigator. + /// + public override XmlNameTable NameTable + { + get { return _nameTable; } + } + + /// + /// Gets the namespace URI of the current node. + /// + public override string NamespaceURI + { + get { return string.Empty; } + } + + /// + /// Gets the XPathNodeType of the current node. + /// + public override XPathNodeType NodeType + { + get + { + DebugEnter("NodeType"); + XPathNodeType type; + + switch (_state.Position) + { + case StatePosition.Macro: + case StatePosition.Parameter: + case StatePosition.ParameterNodes: + type = XPathNodeType.Element; + break; + case StatePosition.ParameterNavigator: + type = _state.ParameterNavigator.NodeType; + break; + case StatePosition.ParameterAttribute: + type = XPathNodeType.Attribute; + break; + case StatePosition.ParameterText: + type = XPathNodeType.Text; + break; + case StatePosition.Root: + type = XPathNodeType.Root; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\'{0}\'", type); + return type; + } + } + + /// + /// Gets the namespace prefix associated with the current node. + /// + public override string Prefix + { + get { return string.Empty; } + } + + /// + /// Gets the string value of the item. + /// + /// Does not fully behave as per the specs, as we report empty value on root and macro elements, and we start + /// reporting values only on parameter elements. This is because, otherwise, we would might dump the whole database + /// and it probably does not make sense at Umbraco level. + public override string Value + { + get + { + DebugEnter("Value"); + string value; + + XPathNavigator nav; + switch (_state.Position) + { + case StatePosition.Parameter: + nav = _macro.Parameters[_state.ParameterIndex].NavigatorValue; + if (nav != null) + { + nav = nav.Clone(); // never use the raw parameter's navigator + nav.MoveToFirstChild(); + value = nav.Value; + } + else + { + var s = _macro.Parameters[_state.ParameterIndex].StringValue; + value = s ?? string.Empty; + } + break; + case StatePosition.ParameterAttribute: + value = _macro.Parameters[_state.ParameterIndex].Attributes[_state.ParameterAttributeIndex].Value; + break; + case StatePosition.ParameterNavigator: + value = _state.ParameterNavigator.Value; + break; + case StatePosition.ParameterNodes: + nav = _macro.Parameters[_state.ParameterIndex].NavigatorValue; + if (nav == null) + value = string.Empty; + else + { + nav = nav.Clone(); // never use the raw parameter's navigator + nav.MoveToFirstChild(); + value = nav.Value; + } + break; + case StatePosition.ParameterText: + value = _macro.Parameters[_state.ParameterIndex].StringValue; + break; + case StatePosition.Macro: + case StatePosition.Root: + value = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", value); + return value; + } + } + + private static bool IsDoc(XPathNavigator nav) + { + if (nav.NodeType != XPathNodeType.Element) + return false; + + var clone = nav.Clone(); + if (!clone.MoveToFirstAttribute()) + return false; + do + { + if (clone.Name == "isDoc") + return true; + } while (clone.MoveToNextAttribute()); + + return false; + } + + #region State management + + // the possible state positions + internal enum StatePosition + { + Root, + Macro, + Parameter, + ParameterAttribute, + ParameterText, + ParameterNodes, + ParameterNavigator + }; + + // gets the state + // for unit tests only + internal State InternalState { get { return _state; } } + + // represents the XPathNavigator state + internal class State + { + public StatePosition Position { get; set; } + + // initialize a new state + private State(StatePosition position) + { + Position = position; + ParameterIndex = 0; + ParameterNavigatorDepth = 0; + ParameterAttributeIndex = 0; + } + + // initialize a new state + // used for creating the very first state + public State() + : this(StatePosition.Root) + { } + + // initialize a clone state + private State(State other) + { + Position = other.Position; + + ParameterIndex = other.ParameterIndex; + + if (Position == StatePosition.ParameterNavigator) + { + ParameterNavigator = other.ParameterNavigator.Clone(); + ParameterNavigatorDepth = other.ParameterNavigatorDepth; + ParameterAttributeIndex = other.ParameterAttributeIndex; + } + } + + public State Clone() + { + return new State(this); + } + + // the index of the current element + public int ParameterIndex { get; set; } + + // the current depth within the element navigator + public int ParameterNavigatorDepth { get; set; } + + // the index of the current element's attribute + public int ParameterAttributeIndex { get; set; } + + // gets or sets the element navigator + public XPathNavigator ParameterNavigator { get; set; } + + // gets a value indicating whether this state is at the same position as another one. + public bool IsSamePosition(State other) + { + return other.Position == Position + && (Position != StatePosition.ParameterNavigator || other.ParameterNavigator.IsSamePosition(ParameterNavigator)) + && other.ParameterIndex == ParameterIndex + && other.ParameterAttributeIndex == ParameterAttributeIndex; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs new file mode 100644 index 0000000000..a17727d4ac --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs @@ -0,0 +1,1149 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.XPath; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Provides a cursor model for navigating Umbraco data as if it were XML. + /// + class NavigableNavigator : XPathNavigator + { + // "The XmlNameTable stores atomized strings of any local name, namespace URI, + // and prefix used by the XPathNavigator. This means that when the same Name is + // returned multiple times (like "book"), the same String object is returned for + // that Name. This makes it possible to write efficient code that does object + // comparisons on these strings, instead of expensive string comparisons." + // + // "When an element or attribute name occurs multiple times in an XML document, + // it is stored only once in the NameTable. The names are stored as common + // language runtime (CLR) object types. This enables you to do object comparisons + // on these strings rather than a more expensive string comparison. These + // string objects are referred to as atomized strings." + // + // But... "Any instance members are not guaranteed to be thread safe." + // + // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx + // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx + // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx + // + // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to + // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI, + // and Prefix properties are returned, the string returned should come from the + // NameTable. Comparisons between names are done by object comparisons rather + // than by string comparisons, which are significantly slower."" + // + // So what shall we do? Well, here we have no namespace, no prefix, and all + // local names come from cached instances of INavigableContentType or + // INavigableFieldType and are already unique. So... create a one nametable + // because we need one, and share it amongst all clones. + + private readonly XmlNameTable _nameTable; + private readonly INavigableSource _source; + private readonly int _lastAttributeIndex; // last index of attributes in the fields collection + private State _state; + + #region Constructor + + /// + /// Initializes a new instance of the class with a content source. + /// + private NavigableNavigator(INavigableSource source) + { + _source = source; + _lastAttributeIndex = source.LastAttributeIndex; + } + + /// + /// Initializes a new instance of the class with a content source, + /// and an optional root content. + /// + /// The content source. + /// The root content. + /// When no root content is supplied then the root of the source is used. + public NavigableNavigator(INavigableSource source, INavigableContent content = null) + : this(source) + { + _nameTable = new NameTable(); + _lastAttributeIndex = source.LastAttributeIndex; + _state = new State(content ?? source.Root, null, null, 0, StatePosition.Root); + } + + /// + /// Initializes a new instance of the class with a content source, a name table and a state. + /// + /// The content source. + /// The name table. + /// The state. + /// Privately used for cloning a navigator. + private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state) + : this(source) + { + _nameTable = nameTable; + _state = state; + } + + #endregion + + #region Diagnostics + + // diagnostics code will not be compiled nor called into Release configuration. + // in Debug configuration, uncomment lines in Debug() to write to console or to log. + +#if DEBUG + private const string Tabs = " "; + private int _tabs; + private readonly int _uid = GetUid(); + private static int _uidg; + private readonly static object Uidl = new object(); + private static int GetUid() + { + lock (Uidl) + { + return _uidg++; + } + } +#endif + + [Conditional("DEBUG")] + void DebugEnter(string name) + { +#if DEBUG + Debug(""); + DebugState(":"); + Debug(name); + _tabs = Math.Min(Tabs.Length, _tabs + 2); +#endif + } + + [Conditional("DEBUG")] + void DebugCreate(NavigableNavigator nav) + { +#if DEBUG + Debug("Create: [NavigableNavigator::{0}]", nav._uid); +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn() + { +#if DEBUG +// ReSharper disable IntroduceOptionalParameters.Local + DebugReturn("(void)"); +// ReSharper restore IntroduceOptionalParameters.Local +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn(bool value) + { +#if DEBUG + DebugReturn(value ? "true" : "false"); +#endif + } + + [Conditional("DEBUG")] + void DebugReturn(string format, params object[] args) + { +#if DEBUG + Debug("=> " + format, args); + if (_tabs > 0) _tabs -= 2; +#endif + } + + [Conditional("DEBUG")] + void DebugState(string s = " =>") + { +#if DEBUG + string position; + + switch (_state.Position) + { + case StatePosition.Attribute: + position = string.Format("At attribute '{0}/@{1}'.", + _state.Content.Type.Name, + _state.FieldIndex < 0 ? "id" : _state.CurrentFieldType.Name); + break; + case StatePosition.Element: + position = string.Format("At element '{0}'.", + _state.Content.Type.Name); + break; + case StatePosition.PropertyElement: + position = string.Format("At property '{0}/{1}'.", + _state.Content.Type.Name, _state.Content.Type.FieldTypes[this._state.FieldIndex].Name); + break; + case StatePosition.PropertyText: + position = string.Format("At property '{0}/{1}' text.", + _state.Content.Type.Name, _state.CurrentFieldType.Name); + break; + case StatePosition.PropertyXml: + position = string.Format("In property '{0}/{1}' xml fragment.", + _state.Content.Type.Name, _state.CurrentFieldType.Name); + break; + case StatePosition.Root: + position = "At root."; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + Debug("State{0} {1}", s, position); +#endif + } + +#if DEBUG + void Debug(string format, params object[] args) + { + // remove comments to write + + format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format; +#pragma warning disable 168 + var msg = string.Format(format, args); // unused if not writing, hence #pragma +#pragma warning restore 168 + //LogHelper.Debug(msg); // beware! this can quicky overflow log4net + //Console.WriteLine(msg); + } +#endif + + #endregion + + /// + /// Gets the underlying content object. + /// + public override object UnderlyingObject + { + get { return _state.Content; } + } + + /// + /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. + /// + /// A new XPathNavigator positioned at the same node as this XPathNavigator. + public override XPathNavigator Clone() + { + DebugEnter("Clone"); + var nav = new NavigableNavigator(_source, _nameTable, _state.Clone()); + DebugCreate(nav); + DebugReturn("[XPathNavigator]"); + return nav; + } + + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator CloneWithNewRoot(string id) + { + DebugEnter("CloneWithNewRoot"); + + int contentId; + State state = null; + + if (id != null && id.Trim() == "-1") + { + state = new State(_source.Root, null, null, 0, StatePosition.Root); + } + else if (int.TryParse(id, out contentId)) + { + var content = _source.Get(contentId); + if (content != null) + { + state = new State(content, null, null, 0, StatePosition.Root); + } + } + + NavigableNavigator clone = null; + + if (state != null) + { + clone = new NavigableNavigator(_source, _nameTable, state); + DebugCreate(clone); + DebugReturn("[XPathNavigator]"); + } + else + { + DebugReturn("[null]"); + } + + return clone; + } + + /// + /// Gets a value indicating whether the current node is an empty element without an end element tag. + /// + public override bool IsEmptyElement + { + get + { + DebugEnter("IsEmptyElement"); + bool isEmpty; + + switch (_state.Position) + { + case StatePosition.Element: + isEmpty = (_state.Content.ChildIds == null || _state.Content.ChildIds.Count == 0) // no content child + && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child + break; + case StatePosition.PropertyElement: + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + isEmpty = _state.Content.Value(_state.FieldIndex) == null; + break; + case StatePosition.PropertyXml: + isEmpty = _state.XmlFragmentNavigator.IsEmptyElement; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.Root: + throw new InvalidOperationException("Not an element."); + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isEmpty); + return isEmpty; + } + } + + /// + /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. + /// + /// The XPathNavigator to compare to this XPathNavigator. + /// true if the two XPathNavigator objects have the same position; otherwise, false. + public override bool IsSamePosition(XPathNavigator nav) + { + DebugEnter("IsSamePosition"); + bool isSame; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + isSame = _state.XmlFragmentNavigator.IsSamePosition(nav); + break; + case StatePosition.Attribute: + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + var other = nav as NavigableNavigator; + isSame = other != null && other._source == _source && _state.IsSamePosition(other._state); + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isSame); + return isSame; + } + + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get + { + DebugEnter("Name"); + string name; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + name = _state.XmlFragmentNavigator.Name; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + name = _state.FieldIndex == -1 ? "id" : _state.CurrentFieldType.Name; + break; + case StatePosition.Element: + name = _state.Content.Type.Name; + break; + case StatePosition.PropertyText: + name = string.Empty; + break; + case StatePosition.Root: + name = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get + { + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Moves the XPathNavigator to the same position as the specified XPathNavigator. + /// + /// The XPathNavigator positioned on the node that you want to move to. + /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveTo(XPathNavigator nav) + { + DebugEnter("MoveTo"); + + var other = nav as NavigableNavigator; + var succ = false; + + if (other != null && other._source == _source) + { + _state = other._state.Clone(); + DebugState(); + succ = true; + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first attribute of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstAttribute() + { + DebugEnter("MoveToFirstAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToFirstAttribute(); + break; + case StatePosition.Element: + _state.FieldIndex = -1; + _state.Position = StatePosition.Attribute; + DebugState(); + succ = true; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first child node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstChild() + { + DebugEnter("MoveToFirstChild"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToFirstChild(); + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + succ = false; + break; + case StatePosition.Element: + var firstPropertyIndex = _lastAttributeIndex + 1; + if (_state.FieldsCount > firstPropertyIndex) + { + _state.Position = StatePosition.PropertyElement; + _state.FieldIndex = firstPropertyIndex; + DebugState(); + succ = true; + } + else succ = MoveToFirstChildElement(); + break; + case StatePosition.PropertyElement: + succ = MoveToFirstChildProperty(); + break; + case StatePosition.Root: + _state.Position = StatePosition.Element; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + private bool MoveToFirstChildElement() + { + var children = _state.Content.ChildIds; + + if (children != null && children.Count > 0) + { + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + var child = children.Select(id => _source.Get(id)).FirstOrDefault(c => c != null); + if (child != null) + { + _state.Position = StatePosition.Element; + _state.FieldIndex = -1; + _state = new State(child, _state, children, 0, StatePosition.Element); + DebugState(); + return true; + } + } + + return false; + } + + private bool MoveToFirstChildProperty() + { + var valueForXPath = _state.Content.Value(_state.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + + var nav = valueForXPath as XPathNavigator; + if (nav != null) + { + nav = nav.Clone(); // never use the one we got + nav.MoveToFirstChild(); + _state.XmlFragmentNavigator = nav; + _state.Position = StatePosition.PropertyXml; + DebugState(); + return true; + } + + if (valueForXPath == null) + return false; + + if (valueForXPath is string) + { + _state.Position = StatePosition.PropertyText; + DebugState(); + return true; + } + + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + } + + /// + /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the first namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToFirstNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the next namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToNextNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves to the node that has an attribute of type ID whose value matches the specified String. + /// + /// A String representing the ID value of the node to which you want to move. + /// true if the XPathNavigator is successful moving; otherwise, false. + /// If false, the position of the navigator is unchanged. + public override bool MoveToId(string id) + { + DebugEnter("MoveToId"); + var succ = false; + + // don't look into fragments, just look for element identifiers + // not sure we actually need to implement it... think of it as + // as exercise of style, always better than throwing NotImplemented. + + int contentId; + if (/*id != null &&*/ id.Trim() == "-1") // id cannot be null + { + _state = new State(_source.Root, null, _source.Root.ChildIds, 0, StatePosition.Element); + succ = true; + } + else if (int.TryParse(id, out contentId)) + { + var content = _source.Get(contentId); + if (content != null) + { + var state = _state; + while (state.Parent != null) + state = state.Parent; + var navRootId = state.Content.Id; // navigator may be rooted below source root + + var s = new Stack(); + while (content != null && content.ParentId != navRootId) + { + s.Push(content); + content = _source.Get(content.ParentId); + } + if (content != null) + { + _state = new State(_source.Root, null, _source.Root.ChildIds, _source.Root.ChildIds.IndexOf(content.Id), StatePosition.Element); + while (content != null) + { + _state = new State(content, _state, content.ChildIds, _state.Content.ChildIds.IndexOf(content.Id), StatePosition.Element); + content = s.Count == 0 ? null : s.Pop(); + } + DebugState(); + succ = true; + } + } + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next sibling node of the current node. + /// + /// true if the XPathNavigator is successful moving to the next sibling node; + /// otherwise, false if there are no more siblings or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNext() + { + DebugEnter("MoveToNext"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToNext(); + break; + case StatePosition.Element: + succ = false; + while (_state.Siblings != null && _state.SiblingIndex < _state.Siblings.Count - 1) + { + // Siblings may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + var node = _source.Get(_state.Siblings[++_state.SiblingIndex]); + if (node == null) continue; + + _state.Content = node; + DebugState(); + succ = true; + break; + } + break; + case StatePosition.PropertyElement: + if (_state.FieldIndex == _state.FieldsCount - 1) + { + // after property elements may come some children elements + // if successful, will push a new state + succ = MoveToFirstChildElement(); + } + else + { + ++_state.FieldIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.PropertyText: + case StatePosition.Attribute: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the previous sibling node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the previous sibling node; + /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToPrevious() + { + DebugEnter("MoveToPrevious"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToPrevious(); + break; + case StatePosition.Element: + succ = false; + while (_state.Siblings != null && _state.SiblingIndex > 0) + { + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + var content = _source.Get(_state.Siblings[--_state.SiblingIndex]); + if (content == null) continue; + + _state.Content = content; + DebugState(); + succ = true; + break; + } + if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex) + { + // before children elements may come some property elements + if (MoveToParentElement()) // pops the state + { + _state.FieldIndex = _state.FieldsCount - 1; + DebugState(); + succ = true; + } + } + break; + case StatePosition.PropertyElement: + succ = false; + if (_state.FieldIndex > _lastAttributeIndex) + { + --_state.FieldIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next attribute. + /// + /// Returns true if the XPathNavigator is successful moving to the next attribute; + /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextAttribute() + { + DebugEnter("MoveToNextAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToNextAttribute(); + break; + case StatePosition.Attribute: + if (_state.FieldIndex == _lastAttributeIndex) + succ = false; + else + { + ++_state.FieldIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the parent node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToParent() + { + DebugEnter("MoveToParent"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Attribute: + case StatePosition.PropertyElement: + _state.Position = StatePosition.Element; + _state.FieldIndex = -1; + DebugState(); + succ = true; + break; + case StatePosition.Element: + succ = MoveToParentElement(); + if (!succ) + { + _state.Position = StatePosition.Root; + succ = true; + } + break; + case StatePosition.PropertyText: + _state.Position = StatePosition.PropertyElement; + DebugState(); + succ = true; + break; + case StatePosition.PropertyXml: + if (!_state.XmlFragmentNavigator.MoveToParent()) + throw new InvalidOperationException("Could not move to parent in fragment."); + if (_state.XmlFragmentNavigator.NodeType == XPathNodeType.Root) + { + _state.XmlFragmentNavigator = null; + _state.Position = StatePosition.PropertyElement; + DebugState(); + } + succ = true; + break; + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + private bool MoveToParentElement() + { + var p = _state.Parent; + if (p != null) + { + _state = p; + DebugState(); + return true; + } + + return false; + } + + /// + /// Moves the XPathNavigator to the root node that the current node belongs to. + /// + public override void MoveToRoot() + { + DebugEnter("MoveToRoot"); + + while (_state.Parent != null) + _state = _state.Parent; + DebugState(); + + DebugReturn(); + } + + /// + /// Gets the base URI for the current node. + /// + public override string BaseURI + { + get { return string.Empty; } + } + + /// + /// Gets the XmlNameTable of the XPathNavigator. + /// + public override XmlNameTable NameTable + { + get { return _nameTable; } + } + + /// + /// Gets the namespace URI of the current node. + /// + public override string NamespaceURI + { + get { return string.Empty; } + } + + /// + /// Gets the XPathNodeType of the current node. + /// + public override XPathNodeType NodeType + { + get + { + DebugEnter("NodeType"); + XPathNodeType type; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + type = _state.XmlFragmentNavigator.NodeType; + break; + case StatePosition.Attribute: + type = XPathNodeType.Attribute; + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + type = XPathNodeType.Element; + break; + case StatePosition.PropertyText: + type = XPathNodeType.Text; + break; + case StatePosition.Root: + type = XPathNodeType.Root; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\'{0}\'", type); + return type; + } + } + + /// + /// Gets the namespace prefix associated with the current node. + /// + public override string Prefix + { + get { return string.Empty; } + } + + /// + /// Gets the string value of the item. + /// + /// Does not fully behave as per the specs, as we report empty value on content elements, and we start + /// reporting values only on property elements. This is because, otherwise, we would dump the whole database + /// and it probably does not make sense at Umbraco level. + public override string Value + { + get + { + DebugEnter("Value"); + string value; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + value = _state.XmlFragmentNavigator.Value; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.PropertyElement: + if (_state.FieldIndex == -1) + { + value = _state.Content.Id.ToString(CultureInfo.InvariantCulture); + } + else + { + var valueForXPath = _state.Content.Value(_state.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + + var nav = valueForXPath as XPathNavigator; + var s = valueForXPath as string; + if (valueForXPath == null) + { + value = string.Empty; + } + else if (nav != null) + { + nav = nav.Clone(); // never use the one we got + value = nav.Value; + } + else if (s != null) + { + value = s; + } + else + { + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + } + } + break; + case StatePosition.Element: + case StatePosition.Root: + value = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", value); + return value; + } + } + + #region State management + + // the possible state positions + internal enum StatePosition + { + Root, + Element, + Attribute, + PropertyElement, + PropertyText, + PropertyXml + }; + + // gets the state + // for unit tests only + internal State InternalState { get { return _state; } } + + // represents the XPathNavigator state + internal class State + { + public StatePosition Position { get; set; } + + // initialize a new state + private State(StatePosition position) + { + Position = position; + FieldIndex = -1; + } + + // initialize a new state + // used for creating the very first state + // and also when moving to a child element + public State(INavigableContent content, State parent, IList siblings, int siblingIndex, StatePosition position) + : this(position) + { + Content = content; + Parent = parent; + Siblings = siblings; + SiblingIndex = siblingIndex; + } + + // initialize a clone state + private State(State other, bool recurse = false) + { + Position = other.Position; + + _content = other._content; + SiblingIndex = other.SiblingIndex; + Siblings = other.Siblings; + FieldsCount = other.FieldsCount; + FieldIndex = other.FieldIndex; + + if (Position == StatePosition.PropertyXml) + XmlFragmentNavigator = other.XmlFragmentNavigator.Clone(); + + // NielsK did + //Parent = other.Parent; + // but that creates corrupted stacks of states when cloning + // because clones share the parents : have to clone the whole + // stack of states. Avoid recursion. + + if (recurse) return; + + var clone = this; + while (other.Parent != null) + { + clone.Parent = new State(other.Parent, true); + clone = clone.Parent; + other = other.Parent; + } + } + + public State Clone() + { + return new State(this); + } + + // the parent state + public State Parent { get; private set; } + + // the current content + private INavigableContent _content; + + // the current content + public INavigableContent Content + { + get + { + return _content; + } + set + { + FieldsCount = value == null ? 0 : value.Type.FieldTypes.Length; + _content = value; + } + } + + // the index of the current content within Siblings + public int SiblingIndex { get; set; } + + // the list of content identifiers for all children of the current content's parent + public IList Siblings { get; set; } + + // the number of fields of the current content + // properties include attributes and properties + public int FieldsCount { get; private set; } + + // the index of the current field + // index -1 means special attribute "id" + public int FieldIndex { get; set; } + + // the current field type + // beware, no check on the index + public INavigableFieldType CurrentFieldType { get { return Content.Type.FieldTypes[FieldIndex]; } } + + // gets or sets the xml fragment navigator + public XPathNavigator XmlFragmentNavigator { get; set; } + + // gets a value indicating whether this state is at the same position as another one. + public bool IsSamePosition(State other) + { + return other.Position == Position + && (Position != StatePosition.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) + && other.Content == Content + && other.FieldIndex == FieldIndex; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/CodeFirst/ContentTypeBase.cs b/src/Umbraco.Tests/CodeFirst/ContentTypeBase.cs index 2de26d87f1..2f175ad668 100644 --- a/src/Umbraco.Tests/CodeFirst/ContentTypeBase.cs +++ b/src/Umbraco.Tests/CodeFirst/ContentTypeBase.cs @@ -93,7 +93,7 @@ namespace Umbraco.Tests.CodeFirst //Using this attribute to hide Properties from Intellisense (when compiled?) [EditorBrowsable(EditorBrowsableState.Never)] - public ICollection Properties + public ICollection Properties { get { return _content.Properties; } } @@ -107,7 +107,7 @@ namespace Umbraco.Tests.CodeFirst //Using this attribute to hide Properties from Intellisense (when compiled?) [EditorBrowsable(EditorBrowsableState.Never)] - public IPublishedContentProperty GetProperty(string alias) + public IPublishedProperty GetProperty(string alias) { return _content.GetProperty(alias); } diff --git a/src/Umbraco.Tests/CodeFirst/ContentTypeMapper.cs b/src/Umbraco.Tests/CodeFirst/ContentTypeMapper.cs index d7ff2c2f94..f09db20db4 100644 --- a/src/Umbraco.Tests/CodeFirst/ContentTypeMapper.cs +++ b/src/Umbraco.Tests/CodeFirst/ContentTypeMapper.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests.CodeFirst foreach (var property in content.Properties) { - var @alias = property.Alias; + var @alias = property.PropertyTypeAlias; var propertyInfo = propertyInfos.FirstOrDefault(x => x.Name.ToUmbracoAlias() == @alias); if (propertyInfo == null) continue; @@ -27,12 +27,12 @@ namespace Umbraco.Tests.CodeFirst object value = null; //TODO Proper mapping of types if (propertyInfo.PropertyType == typeof(string)) - value = property.Value; + value = property.ObjectValue; else if (propertyInfo.PropertyType == typeof(DateTime)) - value = DateTime.Parse(property.Value.ToString()); + value = DateTime.Parse(property.ObjectValue.ToString()); else if (propertyInfo.PropertyType == typeof(Boolean)) { - if (String.IsNullOrEmpty(property.Value.ToString()) || property.Value == "0") + if (String.IsNullOrEmpty(property.ObjectValue.ToString()) || property.ObjectValue == "0") { value = false; } diff --git a/src/Umbraco.Tests/CodeFirst/StronglyTypedMapperTest.cs b/src/Umbraco.Tests/CodeFirst/StronglyTypedMapperTest.cs index 61d33cff9f..79cfad7cbb 100644 --- a/src/Umbraco.Tests/CodeFirst/StronglyTypedMapperTest.cs +++ b/src/Umbraco.Tests/CodeFirst/StronglyTypedMapperTest.cs @@ -1,7 +1,11 @@ -using System.Linq; +using System; +using System.Linq; using System.Text.RegularExpressions; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; using Umbraco.Tests.CodeFirst.TestModels; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; @@ -57,7 +61,31 @@ namespace Umbraco.Tests.CodeFirst #region Test setup public override void Initialize() { + // required so we can access property.Value + //PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver(); + base.Initialize(); + + // need to specify a custom callback for unit tests + // AutoPublishedContentTypes generates properties automatically + // when they are requested, but we must declare those that we + // explicitely want to be here... + + var propertyTypes = new[] + { + // AutoPublishedContentType will auto-generate other properties + new PublishedPropertyType("siteDescription", 0, Guid.Empty), + new PublishedPropertyType("siteName", 0, Guid.Empty), + new PublishedPropertyType("articleContent", 0, Guid.Empty), + new PublishedPropertyType("articleAuthor", 0, Guid.Empty), + new PublishedPropertyType("articleDate", 0, Guid.Empty), + new PublishedPropertyType("pageTitle", 0, Guid.Empty), + }; + var type = new AutoPublishedContentType(0, "anything", propertyTypes); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + Console.WriteLine("INIT STRONG {0}", + PublishedContentType.Get(PublishedItemType.Content, "anything") + .PropertyTypes.Count()); } public override void TearDown() diff --git a/src/Umbraco.Tests/CodeFirst/TestModels/Home.cs b/src/Umbraco.Tests/CodeFirst/TestModels/Home.cs index ffed8cf6c3..e874e5a3b1 100644 --- a/src/Umbraco.Tests/CodeFirst/TestModels/Home.cs +++ b/src/Umbraco.Tests/CodeFirst/TestModels/Home.cs @@ -10,7 +10,7 @@ namespace Umbraco.Tests.CodeFirst.TestModels [PropertyType(typeof(TextFieldDataType))] public string SiteName { get; set; } - [Alias("umbSiteDescription", Name = "Site Description")] + [Alias("umbSiteDescription", Name = "Site Description")] // ignored by the mapper at the moment [PropertyType(typeof(textfieldMultipleDataType))] public string SiteDescription { get; set; } } diff --git a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs new file mode 100644 index 0000000000..2c12d420af --- /dev/null +++ b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs @@ -0,0 +1,1165 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Xml; +using System.Xml.XPath; +using System.Xml.Xsl; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.Xml; +using Umbraco.Core.Xml.XPath; +using NUnit.Framework; + +namespace Umbraco.Tests.CoreXml +{ + [TestFixture] + public class NavigableNavigatorTests + { + [Test] + public void NewNavigatorIsAtRoot() + { + const string xml = @""; + var doc = XmlHelper.CreateXPathDocument(xml); + var nav = doc.CreateNavigator(); + + Assert.AreEqual(XPathNodeType.Root, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + + var source = new TestSource5(); + nav = new NavigableNavigator(source); + + Assert.AreEqual(XPathNodeType.Root, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + } + + [Test] + public void NativeXmlValues() + { + const string xml = @" + + + + + + + + blah + + blah + + + bam + + + +"; + var doc = XmlHelper.CreateXPathDocument(xml); + var nav = doc.CreateNavigator(); + + NavigatorValues(nav, true); + } + + [Test] + public void NavigableXmlValues() + { + var source = new TestSource6(); + var nav = new NavigableNavigator(source); + + NavigatorValues(nav, false); + } + + static void NavigatorValues(XPathNavigator nav, bool native) + { + // in non-native we can't have Value dump everything, else + // we'd dump the entire database? Makes not much sense. + + Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !! + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("root", nav.Name); + Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !! + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("wrap", nav.Name); + Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !! + + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("item1", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); // !! + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2a", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); // !! + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2b", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); // !! + + // we have no way to tell the navigable that a value is CDATA + // so the rule is, if it's null it's not there, anything else is there + // and the filtering has to be done when building the content + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2c", nav.Name); + Assert.AreEqual("\r\n ", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("\r\n ", nav.Value); + Assert.IsTrue(nav.MoveToParent()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item3", nav.Name); + Assert.AreEqual("blah", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("blah", nav.Value); + Assert.IsTrue(nav.MoveToParent()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item3a", nav.Name); + Assert.AreEqual("\r\n blah\r\n ", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("\r\n blah\r\n ", nav.Value); + Assert.IsTrue(nav.MoveToParent()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item4", nav.Name); + Assert.AreEqual("bam", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("subitem", nav.Name); + Assert.AreEqual("bam", nav.Value); // ok since we're in a fragment + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("bam", nav.Value); + Assert.IsFalse(nav.MoveToNext()); + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual("subitem", nav.Name); + Assert.IsFalse(nav.MoveToNext()); + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual("item4", nav.Name); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item5", nav.Name); + Assert.AreEqual("\r\n ", nav.Value); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual("\r\n ", nav.Value); + } + + [Test] + public void Navigate() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + nav.MoveToRoot(); + Assert.AreEqual("", nav.Name); // because we're at root + nav.MoveToFirstChild(); + Assert.AreEqual("root", nav.Name); + nav.MoveToFirstChild(); + Assert.AreEqual("type1", nav.Name); // our first content + nav.MoveToFirstAttribute(); + Assert.AreEqual("id", nav.Name); + Assert.AreEqual("1", nav.Value); + nav.MoveToNextAttribute(); + Assert.AreEqual("prop1", nav.Name); + Assert.AreEqual("1:p1", nav.Value); + nav.MoveToNextAttribute(); + Assert.AreEqual("prop2", nav.Name); + Assert.AreEqual("1:p2", nav.Value); + Assert.IsFalse(nav.MoveToNextAttribute()); + nav.MoveToParent(); + nav.MoveToFirstChild(); + Assert.AreEqual("prop3", nav.Name); + Assert.AreEqual("1:p3", nav.Value); + + Assert.IsFalse(nav.MoveToNext()); + } + + [Test] + public void NavigateMixed() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + nav.MoveToRoot(); + nav.MoveToFirstChild(); + Assert.AreEqual("root", nav.Name); + nav.MoveToFirstChild(); + Assert.AreEqual("type1", nav.Name); // our content + nav.MoveToFirstChild(); + Assert.AreEqual("prop1", nav.Name); // our property + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + nav.MoveToFirstChild(); + + // "poo" + + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("data", nav.Name); + + nav.MoveToFirstChild(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("item1", nav.Name); + + nav.MoveToNext(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("item2", nav.Name); + + nav.MoveToParent(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("data", nav.Name); + + nav.MoveToParent(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("prop1", nav.Name); + } + + [Test] + public void OuterXmlBasic() + { + const string xml = @""; + + var doc = XmlHelper.CreateXPathDocument(xml); + var nnav = doc.CreateNavigator(); + Assert.AreEqual(xml, nnav.OuterXml); + + var source = new TestSource0(); + var nav = new NavigableNavigator(source); + Assert.AreEqual(xml, nav.OuterXml); + } + + [Test] + public void OuterXml() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + const string xml = @" + + 1:p3 + +"; + + Assert.AreEqual(xml, nav.OuterXml); + } + + [Test] + public void OuterXmlMixed() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + nav.MoveToRoot(); + + const string outerXml = @" + + + + poo + + + + + +"; + + Assert.AreEqual(outerXml, nav.OuterXml); + } + + [Test] + public void Query() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("//type1"); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("type1", iterator.Current.Name); + + iterator = nav.Select("//* [@prop1='1:p1']"); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("type1", iterator.Current.Name); + } + + [Test] + public void QueryMixed() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + var doc = XmlHelper.CreateXPathDocument("poo"); + var docNav = doc.CreateNavigator(); + var docIter = docNav.Select("//item2 [@xx=33]"); + Assert.AreEqual(1, docIter.Count); + Assert.AreEqual("", docIter.Current.Name); + docIter.MoveNext(); + Assert.AreEqual("item2", docIter.Current.Name); + + var iterator = nav.Select("//item2 [@xx=33]"); + Assert.AreEqual(1, iterator.Count); + Assert.AreEqual("", iterator.Current.Name); + iterator.MoveNext(); + Assert.AreEqual("item2", iterator.Current.Name); + } + + [Test] + public void QueryWithVariables() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("//* [@prop1=$var]", new XPathVariable("var", "1:p1")); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("type1", iterator.Current.Name); + } + + [Test] + public void QueryMixedWithVariables() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("//item2 [@xx=$var]", new XPathVariable("var", "33")); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("item2", iterator.Current.Name); + } + + [Test] + public void MixedWithNoValue() + { + var source = new TestSource4(); + var nav = new NavigableNavigator(source); + + var doc = XmlHelper.CreateXPathDocument(@" + dang + + + "); + var docNav = doc.CreateNavigator(); + + docNav.MoveToRoot(); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.AreEqual("root", docNav.Name); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.AreEqual("type1", docNav.Name); + Assert.IsTrue(docNav.MoveToNext()); + Assert.AreEqual("type1", docNav.Name); + Assert.IsTrue(docNav.MoveToNext()); + Assert.AreEqual("type1", docNav.Name); + Assert.IsFalse(docNav.MoveToNext()); + + docNav.MoveToRoot(); + var docOuter = docNav.OuterXml; + + nav.MoveToRoot(); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("root", nav.Name); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("type1", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("type1", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("type1", nav.Name); + Assert.IsFalse(nav.MoveToNext()); + + nav.MoveToRoot(); + var outer = nav.OuterXml; + + Assert.AreEqual(docOuter, outer); + } + + [Test] + [Ignore("NavigableNavigator does not implement IHasXmlNode.")] + public void XmlNodeList() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("/*"); + + // but, that requires that the underlying navigator implements IHasXmlNode + // so it is possible to obtain nodes from the navigator - not possible yet + var nodes = XmlNodeListFactory.CreateNodeList(iterator); + + Assert.AreEqual(nodes.Count, 1); + var node = nodes[0]; + + Assert.AreEqual(3, node.Attributes.Count); + Assert.AreEqual("1", node.Attributes["id"].Value); + Assert.AreEqual("1:p1", node.Attributes["prop1"].Value); + Assert.AreEqual("1:p2", node.Attributes["prop2"].Value); + Assert.AreEqual(1, node.ChildNodes.Count); + Assert.AreEqual("prop3", node.FirstChild.Name); + Assert.AreEqual("1:p3", node.FirstChild.Value); + } + + [Test] + public void CloneIsSafe() + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + TestContent content; + + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual("root", nav.Name); // at -1 + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position); + Assert.AreEqual("prop1", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position); + Assert.AreEqual("prop2", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id); + + // at that point nav is at /root/1/3 + + var clone = nav.Clone() as NavigableNavigator; + + // move nav to /root/1/5 and ensure that clone stays at /root/1/3 + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(5, (nav.UnderlyingObject as TestContent).Id); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, clone.InternalState.Position); + Assert.AreEqual(3, (clone.UnderlyingObject as TestContent).Id); + + // move nav to /root/2 + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(2, (nav.UnderlyingObject as TestContent).Id); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position); + Assert.AreEqual("prop1", nav.Name); + Assert.AreEqual("p21", nav.Value); + + // move clone to .. /root/1 + Assert.IsTrue(clone.MoveToParent()); + + // clone has not been corrupted by nav + Assert.AreEqual(NavigableNavigator.StatePosition.Element, clone.InternalState.Position); + Assert.AreEqual(1, (clone.UnderlyingObject as TestContent).Id); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void SelectById(int id) + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + + var iter = nav.Select(string.Format("//* [@id={0}]", id)); + Assert.IsTrue(iter.MoveNext()); + var current = iter.Current as NavigableNavigator; + Assert.AreEqual(NavigableNavigator.StatePosition.Element, current.InternalState.Position); + Assert.AreEqual(id, (current.UnderlyingObject as TestContent).Id); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void SelectByIdWithVariable(int id) + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + + var iter = nav.Select("//* [@id=$id]", new XPathVariable("id", id.ToString(CultureInfo.InvariantCulture))); + Assert.IsTrue(iter.MoveNext()); + var current = iter.Current as NavigableNavigator; + Assert.AreEqual(NavigableNavigator.StatePosition.Element, current.InternalState.Position); + Assert.AreEqual(id, (current.UnderlyingObject as TestContent).Id); + } + + [Test] + public void MoveToId() + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + + // move to /root/1/3 + Assert.IsTrue(nav.MoveToId("3")); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id); + + // move to /root/1 + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // move to /root + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id); + + // move up + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // move to /root/1 + Assert.IsTrue(nav.MoveToId("1")); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // move to /root + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id); + + // move up + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // move to /root + Assert.IsTrue(nav.MoveToId("-1")); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id); + + // move up + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // get lost + Assert.IsFalse(nav.MoveToId("666")); + } + + [Test] + public void RootedNavigator() + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source, source.Get(1)); + + // go to (/root) /1 + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // go to (/root) /1/prop1 + Assert.IsTrue(nav.MoveToFirstChild()); + // go to (/root) /1/prop2 + Assert.IsTrue(nav.MoveToNext()); + // go to (/root) /1/3 + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id); + + // go to (/root) /1 + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // go to (/root) ie root + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // can't go there + Assert.IsFalse(nav.MoveToId("2")); + } + + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void XsltDebugModeAndSortOrder(bool native, bool debug) + { + const string xml = @" + + + title-1 + + title-3 + + title-7 + + + title-8 + + + + title-5 + + + + title-2 + + title-4 + + + title-6 + + + +"; + + const string xslt = @" + +]> + + + + + + + + + +! + + +!! + + +!!! + + + + + + + +"; + const string expected = @"! title-1 +!! title-3 +!!! title-7 +!!! title-8 +!! title-5 +! title-2 +!! title-4 +!! title-6 +"; + + // see http://www.onenaught.com/posts/352/xslt-performance-tip-dont-indent-output + // why aren't we using an XmlWriter here? + + var transform = new XslCompiledTransform(debug); + var xmlReader = new XmlTextReader(new StringReader(xslt)) + { + EntityHandling = EntityHandling.ExpandEntities + }; + var xslResolver = new XmlUrlResolver + { + Credentials = CredentialCache.DefaultCredentials + }; + var args = new XsltArgumentList(); + + // .Default is more restrictive than .TrustedXslt + transform.Load(xmlReader, XsltSettings.Default, xslResolver); + + XPathNavigator macro; + if (!native) + { + var source = new TestSource7(); + var nav = new NavigableNavigator(source); + //args.AddParam("currentPage", string.Empty, nav.Clone()); + + var x = new XmlDocument(); + x.LoadXml(xml); + + macro = new MacroNavigator(new[] + { + // it even fails like that => macro nav. issue? + new MacroNavigator.MacroParameter("nav", x.CreateNavigator()) // nav.Clone()) + } + ); + } + else + { + var doc = new XmlDocument(); + doc.LoadXml(""); + var nav = doc.CreateElement("nav"); + doc.DocumentElement.AppendChild(nav); + var x = new XmlDocument(); + x.LoadXml(xml); + nav.AppendChild(doc.ImportNode(x.DocumentElement, true)); + macro = doc.CreateNavigator(); + } + + var writer = new StringWriter(); + transform.Transform(macro, args, writer); + + // this was working with native, debug and non-debug + // this was working with macro nav, non-debug + // but was NOT working (changing the order of nodes) with macro nav, debug + // was due to an issue with macro nav IsSamePosition, fixed + + //Console.WriteLine("--------"); + //Console.WriteLine(writer.ToString()); + Assert.AreEqual(expected, writer.ToString()); + } + + [Test] + public void WhiteSpacesAndEmptyValues() + { + + // "When Microsoft’s DOM builder receives a text node from the parser + // that contains only white space, it is thrown away." - so if it's ONLY + // spaces, it's nothing, but spaces are NOT trimmed. + + // For attributes, spaces are preserved even when there's only spaces. + + var doc = XmlHelper.CreateXPathDocument(@" + + + + + + ooo + ooo + + + + "); + + var docNav = doc.CreateNavigator(); + + Assert.AreEqual(@" + + + + + + + + + + + + + + ooo + + + ooo + + + + +", docNav.OuterXml); + + docNav.MoveToRoot(); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.IsTrue(docNav.MoveToFirstChild()); // prop + Assert.IsTrue(docNav.IsEmptyElement); + Assert.IsTrue(docNav.MoveToParent()); + Assert.IsTrue(docNav.MoveToNext()); + Assert.IsTrue(docNav.MoveToFirstChild()); // prop + Assert.IsFalse(docNav.IsEmptyElement); + Assert.AreEqual("", docNav.Value); // contains an empty text node + Assert.IsTrue(docNav.MoveToParent()); + Assert.IsTrue(docNav.MoveToNext()); + Assert.IsTrue(docNav.MoveToFirstChild()); // prop + Assert.IsFalse(docNav.IsEmptyElement); + Assert.AreEqual("", docNav.Value); // contains an empty text node + + var source = new TestSource8(); + var nav = new NavigableNavigator(source); + + // shows how whitespaces are handled by NavigableNavigator + Assert.AreEqual(@" + + + + + + + + + + + + + + + ooo + +", nav.OuterXml); + } + } + + #region Navigable implementation + + class TestPropertyType : INavigableFieldType + { + public TestPropertyType(string name, bool isXmlContent = false, Func xmlStringConverter = null) + { + Name = name; + IsXmlContent = isXmlContent; + XmlStringConverter = xmlStringConverter; + } + + public string Name { get; private set; } + public bool IsXmlContent { get; private set; } + public Func XmlStringConverter { get; private set; } + } + + class TestContentType : INavigableContentType + { + public TestContentType(TestSourceBase source, string name, params INavigableFieldType[] properties) + { + Source = source; + Name = name; + FieldTypes = properties; + } + + public TestSourceBase Source { get; private set; } + public string Name { get; private set; } + public INavigableFieldType[] FieldTypes { get; protected set; } + } + + class TestRootContentType : TestContentType + { + public TestRootContentType(TestSourceBase source, params INavigableFieldType[] properties) + : base(source, "root") + { + FieldTypes = properties; + } + + public TestContentType CreateType(string name, params INavigableFieldType[] properties) + { + return new TestContentType(Source, name, FieldTypes.Union(properties).ToArray()); + } + } + + class TestContent : INavigableContent + { + public TestContent(TestContentType type, int id, int parentId) + { + _type = type; + Id = id; + ParentId = parentId; + } + + private readonly TestContentType _type; + public int Id { get; private set; } + public int ParentId { get; private set; } + public INavigableContentType Type { get { return _type; } } + public IList ChildIds { get; private set; } + + public object Value(int id) + { + var fieldType = _type.FieldTypes[id] as TestPropertyType; + if (fieldType == null) throw new Exception("Oops"); + + var value = FieldValues[id]; + var isAttr = id <= _type.Source.LastAttributeIndex; + + // null => return null + if (value == null) return null; + + // attribute => return string value + if (isAttr) return value.ToString(); + + // has a converter => use the converter + if (fieldType.XmlStringConverter != null) + return fieldType.XmlStringConverter(value); + + // not a string => return value as a string + var s = value as string; + if (s == null) return value.ToString(); + + // xml content... try xml + if (fieldType.IsXmlContent) + { + XPathDocument doc; + if (XmlHelper.TryCreateXPathDocumentFromPropertyValue(s, out doc)) + return doc.CreateNavigator(); + } + + // return the string + // even if it's xml that can't be parsed... + return s; + } + + // locals + public object[] FieldValues { get; private set; } + + public TestContent WithChildren(params int[] childIds) + { + ChildIds = childIds; + return this; + } + + public TestContent WithValues(params object[] values) + { + FieldValues = values == null ? new object[] {null} : values; + return this; + } + } + + class TestRootContent : TestContent + { + public TestRootContent(TestContentType type) + : base(type, -1, -1) + { } + } + + abstract class TestSourceBase : INavigableSource + { + protected readonly Dictionary Content = new Dictionary(); + + public INavigableContent Get(int id) + { + return Content.ContainsKey(id) ? Content[id] : null; + } + + public int LastAttributeIndex { get; protected set; } + + public INavigableContent Root { get; protected set; } + } + + #endregion + + #region Navigable sources + + class TestSource0 : TestSourceBase + { + public TestSource0() + { + LastAttributeIndex = -1; + var type = new TestRootContentType(this); + Root = new TestRootContent(type); + } + } + + class TestSource1 : TestSourceBase + { + public TestSource1() + { + // last attribute index is 1 - meaning properties 0 and 1 are attributes, 2+ are elements + // then, fieldValues must have adequate number of items + LastAttributeIndex = 1; + + var prop1 = new TestPropertyType("prop1"); + var prop2 = new TestPropertyType("prop2"); + var prop3 = new TestPropertyType("prop3"); + var type = new TestRootContentType(this, prop1, prop2); + var type1 = type.CreateType("type1", prop3); + + Content[1] = new TestContent(type1, 1, -1).WithValues("1:p1", "1:p2", "1:p3"); + + Root = new TestRootContent(type).WithValues("", "").WithChildren(1); + } + } + + class TestSource2 : TestSourceBase + { + public TestSource2() + { + LastAttributeIndex = -1; + + var prop1 = new TestPropertyType("prop1", true); + var type = new TestRootContentType(this); + var type1 = type.CreateType("type1", prop1); + + const string xml = "poo"; + Content[1] = new TestContent(type1, 1, 1).WithValues(xml); + + Root = new TestRootContent(type).WithChildren(1); + } + } + + class TestSource3 : TestSourceBase + { + public TestSource3() + { + LastAttributeIndex = 1; + + var prop1 = new TestPropertyType("prop1"); + var prop2 = new TestPropertyType("prop2"); + var prop3 = new TestPropertyType("prop3"); + var type = new TestRootContentType(this, prop1, prop2); + var type1 = type.CreateType("type1", prop3); + + Content[1] = new TestContent(type1, 1, 1).WithValues("1:p1", "1:p2", "1:p3").WithChildren(2); + Content[2] = new TestContent(type1, 2, 1).WithValues("2:p1", "2:p2", "2:p3"); + + Root = new TestRootContent(type).WithChildren(1); + } + } + + class TestSource4 : TestSourceBase + { + public TestSource4() + { + LastAttributeIndex = -1; + + var prop1 = new TestPropertyType("prop1", true); + var prop2 = new TestPropertyType("prop2"); + var type = new TestRootContentType(this); + var type1 = type.CreateType("type1", prop1, prop2); + + Content[1] = new TestContent(type1, 1, -1).WithValues("", "dang"); + Content[2] = new TestContent(type1, 2, -1).WithValues(null, ""); + Content[3] = new TestContent(type1, 3, -1).WithValues(null, null); + + Root = new TestRootContent(type).WithChildren(1, 2, 3); + } + } + + class TestSource5 : TestSourceBase + { + public TestSource5() + { + LastAttributeIndex = -1; + + var prop1 = new TestPropertyType("prop1"); + var prop2 = new TestPropertyType("prop2"); + var type = new TestRootContentType(this); + var type1 = type.CreateType("type1", prop1, prop2); + + Content[1] = new TestContent(type1, 1, -1).WithValues("p11", "p12").WithChildren(3, 5); + Content[2] = new TestContent(type1, 2, -1).WithValues("p21", "p22").WithChildren(4, 6); + Content[3] = new TestContent(type1, 3, 1).WithValues("p31", "p32"); + Content[4] = new TestContent(type1, 4, 2).WithValues("p41", "p42"); + Content[5] = new TestContent(type1, 5, 1).WithValues("p51", "p52"); + Content[6] = new TestContent(type1, 6, 2).WithValues("p61", "p62"); + + Root = new TestRootContent(type).WithChildren(1, 2); + } + } + + class TestSource6 : TestSourceBase + { + // + // + // + // + // + // + // + // + // blah + // + // bam + // + // + // + // + // + + public TestSource6() + { + LastAttributeIndex = -1; + + var type = new TestRootContentType(this); + var type1 = type.CreateType("wrap", + new TestPropertyType("item1"), + new TestPropertyType("item2"), + new TestPropertyType("item2a"), + new TestPropertyType("item2b"), + new TestPropertyType("item2c"), + new TestPropertyType("item3"), + new TestPropertyType("item3a"), + new TestPropertyType("item4", true), + new TestPropertyType("item5", true) + ); + + Content[1] = new TestContent(type1, 1, -1) + .WithValues( + null, + null, + null, + null, + "\r\n ", + "blah", + "\r\n blah\r\n ", + "bam", + "\r\n " + ); + + Root = new TestRootContent(type).WithChildren(1); + } + } + + class TestSource7 : TestSourceBase + { + public TestSource7() + { + LastAttributeIndex = 0; + + var prop1 = new TestPropertyType("isDoc"); + var prop2 = new TestPropertyType("title"); + var type = new TestRootContentType(this, prop1); + var type1 = type.CreateType("node", prop1, prop2); + + Content[1] = new TestContent(type1, 1, -1).WithValues(1, "title-1").WithChildren(3, 5); + Content[2] = new TestContent(type1, 2, -1).WithValues(1, "title-2").WithChildren(4, 6); + Content[3] = new TestContent(type1, 3, 1).WithValues(1, "title-3").WithChildren(7, 8); + Content[4] = new TestContent(type1, 4, 2).WithValues(1, "title-4"); + Content[5] = new TestContent(type1, 5, 1).WithValues(1, "title-5"); + Content[6] = new TestContent(type1, 6, 2).WithValues(1, "title-6"); + + Content[7] = new TestContent(type1, 7, 3).WithValues(1, "title-7"); + Content[8] = new TestContent(type1, 8, 3).WithValues(1, "title-8"); + + Root = new TestRootContent(type).WithValues(null).WithChildren(1, 2); + } + } + + class TestSource8 : TestSourceBase + { + public TestSource8() + { + LastAttributeIndex = 0; + + var attr = new TestPropertyType("attr"); + var prop = new TestPropertyType("prop"); + var type = new TestRootContentType(this, attr); + var type1 = type.CreateType("item", attr, prop); + Content[1] = new TestContent(type1, 1, -1).WithValues(null, null); + Content[2] = new TestContent(type1, 2, -1).WithValues("", ""); + Content[3] = new TestContent(type1, 3, -1).WithValues(" ", " "); + Content[4] = new TestContent(type1, 4, -1).WithValues("", "\r\n"); + Content[5] = new TestContent(type1, 5, -1).WithValues(" ooo ", " ooo "); + Root = new TestRootContent(type).WithValues(null).WithChildren(1, 2, 3, 4, 5); + } + } + + #endregion +} diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs new file mode 100644 index 0000000000..003eb31d78 --- /dev/null +++ b/src/Umbraco.Tests/DynamicsAndReflection/ExtensionMethodFinderTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Dynamics; + +namespace Umbraco.Tests.DynamicsAndReflection +{ + [TestFixture] + public class ExtensionMethodFinderTests + { + // To expand on Jon's answer, the reason this doesn't work is because in regular, + // non-dynamic code extension methods work by doing a full search of all the + // classes known to the compiler for a static class that has an extension method + // that match. The search goes in order based on the namespace nesting and available + // "using" directives in each namespace. + // + // That means that in order to get a dynamic extension method invocation resolved + // correctly, somehow the DLR has to know at runtime what all the namespace nestings + // and "using" directives were in your source code. We do not have a mechanism handy + // for encoding all that information into the call site. We considered inventing + // such a mechanism, but decided that it was too high cost and produced too much + // schedule risk to be worth it. + // + // Eric Lippert, http://stackoverflow.com/questions/5311465/extension-method-and-dynamic-object-in-c-sharp + + [Test] + [Ignore("fails")] + public void TypesTests() + { + Assert.IsTrue(typeof(int[]).Inherits()); + Assert.IsFalse(typeof(int[]).Inherits()); + + var m1 = typeof (ExtensionMethodFinderTests).GetMethod("TestMethod1"); + + var a1A = new object[] {1}; + var m1A = GetMethodForArguments(m1, a1A); + Assert.IsNotNull(m1A); + m1A.Invoke(this, a1A); + + var a1B = new object[] {"foo"}; + var m1B = GetMethodForArguments(m1, a1B); + Assert.IsNull(m1B); + + var m2 = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod2"); + + var m2A = GetMethodForArguments(m2, a1A); + Assert.IsNotNull(m2A); + m2A.Invoke(this, a1A); + + var m2B = GetMethodForArguments(m2, a1B); + Assert.IsNotNull(m2B); + m2B.Invoke(this, a1B); + + var m3 = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod3"); + + var a3A = new object[] {1, 2}; + var m3A = GetMethodForArguments(m3, a3A); + Assert.IsNotNull(m3A); + m3A.Invoke(this, a3A); + + var a3B = new object[] {1, "foo"}; + var m3B = GetMethodForArguments(m3, a3B); + Assert.IsNull(m3B); + + var m4 = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod4"); + + var m4A = GetMethodForArguments(m4, a3A); + Assert.IsNotNull(m4A); + m4A.Invoke(this, a3A); + + var m4B = GetMethodForArguments(m4, a3B); + Assert.IsNotNull(m4B); + m4B.Invoke(this, a3B); + + var m5 = typeof(ExtensionMethodFinderTests).GetMethod("TestMethod5"); + + // note - currently that fails because we can't match List with List + var a5 = new object[] {new List()}; + var m5A = GetMethodForArguments(m5, a5); + Assert.IsNotNull(m5A); + + // note - should we also handle "ref" and "out" parameters? + // note - should we pay attention to array types? + } + + public void TestMethod1(int value) {} + public void TestMethod2(T value) {} + public void TestMethod3(T value1, T value2) { } + public void TestMethod4(T1 value1, T2 value2) { } + public void TestMethod5(List value) { } + + // gets the method that can apply to the arguments + // either the method itself, or a generic one + // or null if it couldn't match + // + // this is a nightmare - if we want to do it right, then we have + // to re-do the whole compiler type inference stuff by ourselves?! + // + static MethodInfo GetMethodForArguments(MethodInfo method, IList arguments) + { + var parameters = method.GetParameters(); + var genericArguments = method.GetGenericArguments(); + + if (parameters.Length != arguments.Count) return null; + + var genericArgumentTypes = new Type[genericArguments.Length]; + var i = 0; + for (; i < parameters.Length; i++) + { + var parameterType = parameters[i].ParameterType; + var argumentType = arguments[i].GetType(); + + Console.WriteLine("{0} / {1}", parameterType, argumentType); + + if (parameterType == argumentType) continue; // match + if (parameterType.IsGenericParameter) // eg T + { + var pos = parameterType.GenericParameterPosition; + if (genericArgumentTypes[pos] != null) + { + // note - is this OK? what about variance and such? + // it is NOT ok, if the first pass is SomethingElse then next is Something + // it will fail... the specs prob. indicate how it works, trying to find a common + // type... + if (genericArgumentTypes[pos].IsAssignableFrom(argumentType) == false) + break; + } + else + { + genericArgumentTypes[pos] = argumentType; + } + } + else if (parameterType.IsGenericType) // eg List + { + if (argumentType.IsGenericType == false) break; + + var pg = parameterType.GetGenericArguments(); + var ag = argumentType.GetGenericArguments(); + + // then what ?! + // should _variance_ be of some importance? + Console.WriteLine("generic {0}", argumentType.IsGenericType); + } + else + { + if (parameterType.IsAssignableFrom(argumentType) == false) + break; + } + } + if (i != parameters.Length) return null; + return genericArguments.Length == 0 + ? method + : method.MakeGenericMethod(genericArgumentTypes); + } + + public class Class1 + {} + + [Test] + [Ignore("fails")] + public void FinderTests() + { + MethodInfo method; + var class1 = new Class1(); + + method = ExtensionMethodFinder.FindExtensionMethod(typeof (Class1), new object[] {1}, "TestMethod1", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, 1 }); + + method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod1", false); + Assert.IsNull(method); // note - fails, return TestMethod1! + + method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { 1 }, "TestMethod2", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "1" }); + + method = ExtensionMethodFinder.FindExtensionMethod(typeof(Class1), new object[] { "x" }, "TestMethod2", false); + Assert.IsNotNull(method); + method.Invoke(null, new object[] { class1, "x" }); + } + } + + static class ExtensionMethodFinderTestsExtensions + { + public static void TestMethod1(this ExtensionMethodFinderTests.Class1 source, int value) + { } + + public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, int value) + { } + + public static void TestMethod2(this ExtensionMethodFinderTests.Class1 source, string value) + { } + } +} diff --git a/src/Umbraco.Tests/DynamicsAndReflection/ReflectionTests.cs b/src/Umbraco.Tests/DynamicsAndReflection/ReflectionTests.cs new file mode 100644 index 0000000000..857517724f --- /dev/null +++ b/src/Umbraco.Tests/DynamicsAndReflection/ReflectionTests.cs @@ -0,0 +1,73 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Core; + +namespace Umbraco.Tests.DynamicsAndReflection +{ + [TestFixture] + public class ReflectionTests + { + [Test] + public void GetBaseTypesIsOk() + { + // tests that the GetBaseTypes extension method works. + + var type = typeof(Class2); + var types = type.GetBaseTypes(true).ToArray(); + Assert.AreEqual(3, types.Length); + Assert.Contains(typeof(Class2), types); + Assert.Contains(typeof(Class1), types); + Assert.Contains(typeof(object), types); + + types = type.GetBaseTypes(false).ToArray(); + Assert.AreEqual(2, types.Length); + Assert.Contains(typeof(Class1), types); + Assert.Contains(typeof(object), types); + } + + [Test] + public void GetInterfacesIsOk() + { + // tests that GetInterfaces gets _all_ interfaces + // so the AllInterfaces extension method is useless + + var type = typeof(Class2); + var interfaces = type.GetInterfaces(); + Assert.AreEqual(2, interfaces.Length); + Assert.Contains(typeof(IInterface1), interfaces); + Assert.Contains(typeof(IInterface2), interfaces); + } + + // TypeExtensions.AllInterfaces was broken an not used, has been commented out + // + //[Test] + //public void AllInterfacesIsBroken() + //{ + // // tests that the AllInterfaces extension method is broken + // + // var type = typeof(Class2); + // var interfaces = type.AllInterfaces().ToArray(); + // Assert.AreEqual(3, interfaces.Length); // should be 2! + // Assert.Contains(typeof(IInterface1), interfaces); + // Assert.Contains(typeof(IInterface2), interfaces); + // Assert.AreEqual(2, interfaces.Count(i => i == typeof(IInterface1))); // duplicate! + // Assert.AreEqual(1, interfaces.Count(i => i == typeof(IInterface2))); + //} + + interface IInterface1 + { } + + interface IInterface2 : IInterface1 + { + void Method(); + } + + class Class1 : IInterface2 + { + public void Method() { } + } + + class Class2 : Class1 + { } + } +} diff --git a/src/Umbraco.Tests/LibraryTests.cs b/src/Umbraco.Tests/LibraryTests.cs index 100e7b8b34..c724ff0a29 100644 --- a/src/Umbraco.Tests/LibraryTests.cs +++ b/src/Umbraco.Tests/LibraryTests.cs @@ -4,7 +4,11 @@ using System.IO; using System.Linq; using System.Text; using NUnit.Framework; +using Umbraco.Core.Models; using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Web; using Umbraco.Web.PublishedCache; @@ -20,11 +24,30 @@ namespace Umbraco.Tests [TestFixture] public class LibraryTests : BaseRoutingTest { - public override void Initialize() - { - base.Initialize(); + public override void Initialize() + { + // required so we can access property.Value + PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver(); + + base.Initialize(); - var routingContext = GetRoutingContext("/test", 1234); + // need to specify a custom callback for unit tests + // AutoPublishedContentTypes generates properties automatically + // when they are requested, but we must declare those that we + // explicitely want to be here... + + var propertyTypes = new[] + { + // AutoPublishedContentType will auto-generate other properties + new PublishedPropertyType("content", 0, Guid.Empty), + }; + var type = new AutoPublishedContentType(0, "anything", propertyTypes); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + Console.WriteLine("INIT LIB {0}", + PublishedContentType.Get(PublishedItemType.Content, "anything") + .PropertyTypes.Count()); + + var routingContext = GetRoutingContext("/test", 1234); UmbracoContext.Current = routingContext.UmbracoContext; } diff --git a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs index e31d16800a..5d5c726270 100644 --- a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -1,6 +1,7 @@ using System; using NUnit.Framework; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.PropertyEditors @@ -8,42 +9,54 @@ namespace Umbraco.Tests.PropertyEditors [TestFixture] public class PropertyEditorValueConverterTests { - [TestCase("2012-11-10", true)] - [TestCase("2012/11/10", true)] - [TestCase("10/11/2012", true)] - [TestCase("11/10/2012", false)] - [TestCase("Sat 10, Nov 2012", true)] - [TestCase("Saturday 10, Nov 2012", true)] - [TestCase("Sat 10, November 2012", true)] - [TestCase("Saturday 10, November 2012", true)] - [TestCase("2012-11-10 13:14:15", true)] - [TestCase("", false)] + // see notes in the converter + // only ONE date format is expected here + + //[TestCase("2012-11-10", true)] + //[TestCase("2012/11/10", true)] + //[TestCase("10/11/2012", true)] + //[TestCase("11/10/2012", false)] + //[TestCase("Sat 10, Nov 2012", true)] + //[TestCase("Saturday 10, Nov 2012", true)] + //[TestCase("Sat 10, November 2012", true)] + //[TestCase("Saturday 10, November 2012", true)] + //[TestCase("2012-11-10 13:14:15", true)] + [TestCase("2012-11-10 13:14:15", false)] + [TestCase("2012-11-10T13:14:15", true)] + [TestCase("", false)] public void CanConvertDatePickerPropertyEditor(string date, bool expected) { - var converter = new DatePickerPropertyValueConverter(); + var converter = new DatePickerValueConverter(); var dateTime = new DateTime(2012, 11, 10, 13, 14, 15); - var result = converter.ConvertSourceToObject(date, null, false); + var result = converter.ConvertDataToSource(null, date, false); // does not use type for conversion - Assert.IsTrue(result.Success); - Assert.AreEqual(DateTime.Equals(dateTime.Date, ((DateTime) result.Result).Date), expected); - } + if (expected) + Assert.AreEqual(dateTime.Date, ((DateTime) result).Date); + else + Assert.AreNotEqual(dateTime.Date, ((DateTime)result).Date); + } - [TestCase("TRUE", true)] - [TestCase("True", true)] - [TestCase("true", true)] + // see the notes in the converter + // values such as "true" are NOT expected here + + //[TestCase("TRUE", true)] + //[TestCase("True", true)] + //[TestCase("true", true)] [TestCase("1", true)] - [TestCase("FALSE", false)] - [TestCase("False", false)] - [TestCase("false", false)] + //[TestCase("FALSE", false)] + //[TestCase("False", false)] + //[TestCase("false", false)] [TestCase("0", false)] [TestCase("", false)] - public void CanConvertYesNoPropertyEditor(string value, bool expected) + [TestCase("true", false)] + [TestCase("false", false)] + [TestCase("blah", false)] + public void CanConvertYesNoPropertyEditor(string value, bool expected) { - var converter = new YesNoPropertyValueConverter(); - var result = converter.ConvertSourceToObject(value, null, false); + var converter = new YesNoValueConverter(); + var result = converter.ConvertDataToSource(null, value, false); // does not use type for conversion - Assert.IsTrue(result.Success); - Assert.AreEqual(expected, result.Result); + Assert.AreEqual(expected, result); } } } diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicDocumentTestsBase.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicDocumentTestsBase.cs deleted file mode 100644 index e0321e08d5..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicDocumentTestsBase.cs +++ /dev/null @@ -1,699 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Dynamics; -using Umbraco.Tests.TestHelpers; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public abstract class DynamicDocumentTestsBase : PublishedContentTestBase - { - protected override DatabaseBehavior DatabaseTestBehavior - { - get { return DatabaseBehavior.NoDatabasePerFixture; } - } - - protected override string GetXmlContent(int templateId) - { - return @" - - - - -]> - - - - - 1 - - - This is some content]]> - - - - - - - - - - - - 1 - - - - - - - - - - - -"; - } - - /// - /// Returns the dynamic node/document to run tests against - /// - /// - /// - protected abstract dynamic GetDynamicNode(int id); - - [Test] - public void Recursive_Property() - { - var doc = GetDynamicNode(1174); - var prop = doc.GetProperty("siteTitle", true); - Assert.IsNotNull(prop); - Assert.AreEqual("This is my site", prop.Value); - prop = doc.GetProperty("_siteTitle"); //test with underscore prefix - Assert.IsNotNull(prop); - Assert.AreEqual("This is my site", prop.Value); - Assert.AreEqual("This is my site", doc._siteTitle); - } - - /// - /// Tests the internal instance level caching of returning properties - /// - /// - /// http://issues.umbraco.org/issue/U4-1824 - /// http://issues.umbraco.org/issue/U4-1825 - /// - [Test] - public void Can_Return_Property_And_Value() - { - var doc = GetDynamicNode(1173); - - Assert.IsTrue(doc.HasProperty(Constants.Conventions.Content.UrlAlias)); - var prop = doc.GetProperty(Constants.Conventions.Content.UrlAlias); - Assert.IsNotNull(prop); - Assert.AreEqual("page2/alias, 2ndpagealias", prop.Value); - Assert.AreEqual("page2/alias, 2ndpagealias", doc.umbracoUrlAlias); - } - - /// - /// Tests the IsLast method with the result set from a Where statement - /// - [Test] - public void Is_Last_From_Where_Filter() - { - var doc = GetDynamicNode(1173); - - foreach (var d in doc.Children.Where("Visible")) - { - if (d.Id != 1178) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - - } - - [Test] - public void Single() - { - var doc = GetDynamicNode(4444); - - var result = doc.Children().Single(); - - Assert.IsNotNull(result); - Assert.AreEqual(5555, result.Id); - } - - [Test] - public void Single_With_Query() - { - var doc = GetDynamicNode(1046); - - var result = doc.Children().Single("id==1175"); - - Assert.IsNotNull(result); - Assert.AreEqual(1175, result.Id); - } - - [Test] - public void First() - { - var doc = GetDynamicNode(1173); - - var result = doc.Children().First(); - - Assert.IsNotNull(result); - Assert.AreEqual(1174, result.Id); - } - - [Test] - public void First_With_Query() - { - var doc = GetDynamicNode(1173); - - var result = doc.Children().First("blah==\"some content\""); - - Assert.IsNotNull(result); - Assert.AreEqual(1176, result.Id); - } - - [Test] - public void Where_User_Property_Value() - { - var doc = GetDynamicNode(1173); - - var result = (IEnumerable)doc.Children().Where("blah==\"some content\""); - - Assert.IsNotNull(result); - Assert.AreEqual(1, result.Count()); - Assert.AreEqual(1176, result.Single().Id); - } - - [Test] - public void String_ContainsValue_Extension_Method() - { - var doc = GetDynamicNode(1046); - - var paramVals = new Dictionary { { "searchId", 1173 } }; //this is an integer value - var result = doc.Children() - .Where("selectedNodes.ContainsValue(searchId)", paramVals) //call an extension method - .FirstOrDefault(); - - Assert.IsNotNull(result); - Assert.AreEqual(4444, result.Id); - - //don't find! - paramVals = new Dictionary { { "searchId", 1111777 } }; - result = doc.Children() - .Where("selectedNodes.ContainsValue(searchId)", paramVals) - .FirstOrDefault(); - - Assert.IsNotNull(result); - Assert.IsTrue(result.GetType() == typeof(DynamicNull) || result.GetType() == typeof(umbraco.MacroEngines.DynamicNull)); - //Assert.AreEqual(typeof(DynamicNull), result.GetType()); - } - - [Test] - public void String_Contains_Method() - { - var doc = GetDynamicNode(1046); - - var paramVals = new Dictionary { { "searchId", "1173" } }; - var result = doc.Children() - .Where("selectedNodes.Contains(searchId)", paramVals) - .FirstOrDefault(); - - Assert.IsNotNull(result); - Assert.AreEqual(4444, result.Id); - - //don't find! - paramVals = new Dictionary { { "searchId", "1aaa173" } }; - result = doc.Children() - .Where("selectedNodes.Contains(searchId)", paramVals) - .FirstOrDefault(); - - Assert.IsNotNull(result); - Assert.IsTrue(result.GetType() == typeof (DynamicNull) || result.GetType() == typeof (umbraco.MacroEngines.DynamicNull)); - //Assert.AreEqual(typeof (DynamicNull), result.GetType()); - } - - [Test] - public void String_Split_Method() - { - var doc = GetDynamicNode(1046); - - var paramVals = new Dictionary - { - { "splitTerm", new char[] { ',' } }, - { "splitOptions", StringSplitOptions.RemoveEmptyEntries } - }; - var result = doc.Children() - .Where("selectedNodes.Split(splitTerm, splitOptions).Length == 3", paramVals) - .FirstOrDefault(); - - Assert.IsNotNull(result); - Assert.AreEqual(4444, result.Id); - } - - [Ignore("We are ignoring this test because currently our ExpressionParser class cannot deal with this... it needs some serious TLC but it is very complex.")] - [Test] - public void Complex_Linq() - { - var doc = GetDynamicNode(1173); - - var paramVals = new Dictionary {{"splitTerm", new char[] {','}}, {"searchId", "1173"}}; - var result = doc.Ancestors().OrderBy("level") - .Single() - .Descendants() - .Where("selectedNodes != null && selectedNodes != String.Empty && selectedNodes.Split(splitTerm).Contains(searchId)", paramVals) - .FirstOrDefault(); - - Assert.IsNotNull(result); - Assert.AreEqual(4444, result.Id); - } - - [Test] - public void Index() - { - var doc = GetDynamicNode(1173); - Assert.AreEqual(0, doc.Index()); - doc = GetDynamicNode(1176); - Assert.AreEqual(3, doc.Index()); - doc = GetDynamicNode(1177); - Assert.AreEqual(1, doc.Index()); - doc = GetDynamicNode(1178); - Assert.AreEqual(2, doc.Index()); - } - - [Test] - public virtual void Is_First_Root_Nodes() - { - var doc = GetDynamicNode(1046); //test root nodes - Assert.IsTrue(doc.IsFirst()); - doc = GetDynamicNode(1172); - Assert.IsFalse(doc.IsFirst()); - } - - [Test] - public void Is_First() - { - var doc = GetDynamicNode(1173); //test normal nodes - Assert.IsTrue(doc.IsFirst()); - doc = GetDynamicNode(1175); - Assert.IsFalse(doc.IsFirst()); - } - - [Test] - public virtual void Is_Not_First_Root_Nodes() - { - var doc = GetDynamicNode(1046); //test root nodes - Assert.IsFalse(doc.IsNotFirst()); - doc = GetDynamicNode(1172); - Assert.IsTrue(doc.IsNotFirst()); - } - - [Test] - public void Is_Not_First() - { - var doc = GetDynamicNode(1173); //test normal nodes - Assert.IsFalse(doc.IsNotFirst()); - doc = GetDynamicNode(1175); - Assert.IsTrue(doc.IsNotFirst()); - } - - [Test] - public virtual void Is_Position_Root_Nodes() - { - var doc = GetDynamicNode(1046); //test root nodes - Assert.IsTrue(doc.IsPosition(0)); - doc = GetDynamicNode(1172); - Assert.IsTrue(doc.IsPosition(1)); - } - - [Test] - public void Is_Position() - { - var doc = GetDynamicNode(1173); //test normal nodes - Assert.IsTrue(doc.IsPosition(0)); - doc = GetDynamicNode(1175); - Assert.IsTrue(doc.IsPosition(1)); - } - - [Test] - public void Children_GroupBy_DocumentTypeAlias() - { - var doc = GetDynamicNode(1046); - - var found1 = doc.Children.GroupBy("DocumentTypeAlias"); - - var casted = (IEnumerable>)(found1); - Assert.AreEqual(2, casted.Count()); - Assert.AreEqual(2, casted.Single(x => x.Key.ToString() == "Home").Count()); - Assert.AreEqual(1, casted.Single(x => x.Key.ToString() == "CustomDocument").Count()); - } - - [Test] - public void Children_Where_DocumentTypeAlias() - { - var doc = GetDynamicNode(1046); - - var found1 = doc.Children.Where("DocumentTypeAlias == \"CustomDocument\""); - var found2 = doc.Children.Where("DocumentTypeAlias == \"Home\""); - - Assert.AreEqual(1, found1.Count()); - Assert.AreEqual(2, found2.Count()); - } - - [Test] - public void Children_Where_NodeTypeAlias() - { - var doc = GetDynamicNode(1046); - - var found1 = doc.Children.Where("NodeTypeAlias == \"CustomDocument\""); - var found2 = doc.Children.Where("NodeTypeAlias == \"Home\""); - - Assert.AreEqual(1, found1.Count()); - Assert.AreEqual(2, found2.Count()); - } - - [Test] - public void Children_Order_By_Update_Date() - { - var asDynamic = GetDynamicNode(1173); - - var ordered = asDynamic.Children.OrderBy("UpdateDate"); - var casted = (IEnumerable)ordered; - - var correctOrder = new[] { 1178, 1177, 1174, 1176 }; - for (var i = 0; i < correctOrder.Length ;i++) - { - Assert.AreEqual(correctOrder[i], ((dynamic)casted.ElementAt(i)).Id); - } - - } - - [Test] - public void Children_Order_By_Update_Date_Descending() - { - var asDynamic = GetDynamicNode(1173); - - var ordered = asDynamic.Children.OrderBy("UpdateDate desc"); - var casted = (IEnumerable)ordered; - - var correctOrder = new[] { 1176, 1174, 1177, 1178 }; - for (var i = 0; i < correctOrder.Length; i++) - { - Assert.AreEqual(correctOrder[i], ((dynamic)casted.ElementAt(i)).Id); - } - - } - - [Test] - public void HasProperty() - { - var asDynamic = GetDynamicNode(1173); - - var hasProp = asDynamic.HasProperty(Constants.Conventions.Content.UrlAlias); - - Assert.AreEqual(true, (bool)hasProp); - - } - - [Test] - public void Skip() - { - var asDynamic = GetDynamicNode(1173); - - var skip = asDynamic.Children.Skip(2); - var casted = (IEnumerable)skip; - - Assert.AreEqual(2, casted.Count()); - Assert.IsTrue(casted.Select(x => ((dynamic) x).Id).ContainsAll(new dynamic[] {1178, 1176})); - - } - - [Test] - public void HasValue() - { - var asDynamic = GetDynamicNode(1173); - - var hasValue = asDynamic.HasValue(Constants.Conventions.Content.UrlAlias); - var noValue = asDynamic.HasValue("blahblahblah"); - - Assert.IsTrue(hasValue); - Assert.IsFalse(noValue); - } - - [Test] - public void Take() - { - var asDynamic = GetDynamicNode(1173); - - var take = asDynamic.Children.Take(2); - var casted = (IEnumerable)take; - - Assert.AreEqual(2, casted.Count()); - Assert.IsTrue(casted.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1174, 1177 })); - } - - [Test] - public void Ancestors_Where_Visible() - { - var asDynamic = GetDynamicNode(1174); - - var whereVisible = asDynamic.Ancestors().Where("Visible"); - var casted = (IEnumerable)whereVisible; - - Assert.AreEqual(1, casted.Count()); - - } - - [Test] - public void Visible() - { - var asDynamicHidden = GetDynamicNode(1046); - var asDynamicVisible = GetDynamicNode(1173); - - Assert.IsFalse(asDynamicHidden.Visible); - Assert.IsTrue(asDynamicVisible.Visible); - } - - [Test] - public void Ensure_TinyMCE_Converted_Type_User_Property() - { - var asDynamic = GetDynamicNode(1173); - - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(asDynamic.Content.GetType())); - Assert.AreEqual("
This is some content
", asDynamic.Content.ToString()); - } - - [Test] - public void Get_Children_With_Pluralized_Alias() - { - var asDynamic = GetDynamicNode(1173); - - Action doAssert = d => - { - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(d)); - var casted = (IEnumerable)d; - Assert.AreEqual(2, casted.Count()); - }; - - doAssert(asDynamic.Homes); //pluralized alias - doAssert(asDynamic.homes); //pluralized alias - doAssert(asDynamic.CustomDocuments); //pluralized alias - doAssert(asDynamic.customDocuments); //pluralized alias - } - - [Test] - public void GetPropertyValue_Non_Reflected() - { - var asDynamic = GetDynamicNode(1174); - - Assert.AreEqual("Custom data with same property name as the member name", asDynamic.GetPropertyValue("creatorName")); - Assert.AreEqual("Custom data with same property name as the member name", asDynamic.GetPropertyValue("CreatorName")); - } - - [Test] - public void GetPropertyValue_Reflected() - { - var asDynamic = GetDynamicNode(1174); - - Assert.AreEqual("admin", asDynamic.GetPropertyValue("@creatorName")); - Assert.AreEqual("admin", asDynamic.GetPropertyValue("@CreatorName")); - } - - [Test] - public void Get_User_Property_With_Same_Name_As_Member_Property() - { - var asDynamic = GetDynamicNode(1174); - - Assert.AreEqual("Custom data with same property name as the member name", asDynamic.creatorName); - - //because CreatorName is defined on DynamicNode, it will not return the user defined property - Assert.AreEqual("admin", asDynamic.CreatorName); - } - - [Test] - public void Get_Member_Property() - { - var asDynamic = GetDynamicNode(1173); - - Assert.AreEqual((int) 2, (int) asDynamic.Level); - Assert.AreEqual((int) 2, (int) asDynamic.level); - - Assert.AreEqual((int) 1046, (int) asDynamic.ParentId); - Assert.AreEqual((int) 1046, (int) asDynamic.parentId); - } - - [Test] - public void Get_Children() - { - var asDynamic = GetDynamicNode(1173); - - var children = asDynamic.Children; - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(children)); - - var childrenAsList = asDynamic.ChildrenAsList; //test ChildrenAsList too - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(childrenAsList)); - - var castChildren = (IEnumerable)children; - Assert.AreEqual(4, castChildren.Count()); - - var castChildrenAsList = (IEnumerable)childrenAsList; - Assert.AreEqual(4, castChildrenAsList.Count()); - } - - [Test] - public void Ancestor_Or_Self() - { - var asDynamic = GetDynamicNode(1173); - - var result = asDynamic.AncestorOrSelf(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int) 1046, (int) result.Id); - } - - [Test] - public void Ancestors_Or_Self() - { - var asDynamic = GetDynamicNode(1174); - - var result = asDynamic.AncestorsOrSelf(); - - Assert.IsNotNull(result); - - var list = (IEnumerable)result; - Assert.AreEqual(3, list.Count()); - Assert.IsTrue(list.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1174, 1173, 1046 })); - } - - [Test] - public void Ancestors() - { - var asDynamic = GetDynamicNode(1174); - - var result = asDynamic.Ancestors(); - - Assert.IsNotNull(result); - - var list = (IEnumerable)result; - Assert.AreEqual(2, list.Count()); - Assert.IsTrue(list.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1173, 1046 })); - } - - [Test] - public void Descendants_Or_Self() - { - var asDynamic = GetDynamicNode(1046); - - var result = asDynamic.DescendantsOrSelf(); - - Assert.IsNotNull(result); - - var list = (IEnumerable)result; - Assert.AreEqual(9, list.Count()); - Assert.IsTrue(list.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1046, 1173, 1174, 1176, 1175, 4444 })); - } - - [Test] - public void Descendants() - { - var asDynamic = GetDynamicNode(1046); - - var result = asDynamic.Descendants(); - - Assert.IsNotNull(result); - - var list = (IEnumerable)result; - Assert.AreEqual(8, list.Count()); - Assert.IsTrue(list.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1173, 1174, 1176, 1175, 4444 })); - } - - [Test] - public void Up() - { - var asDynamic = GetDynamicNode(1173); - - var result = asDynamic.Up(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int) 1046, (int) result.Id); - } - - [Test] - public void Down() - { - var asDynamic = GetDynamicNode(1173); - - var result = asDynamic.Down(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int) 1174, (int) result.Id); - } - - [Test] - public void Next() - { - var asDynamic = GetDynamicNode(1173); - - var result = asDynamic.Next(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int) 1175, (int) result.Id); - } - - [Test] - public void Next_Without_Sibling() - { - var asDynamic = GetDynamicNode(1176); - - Assert.IsNull(asDynamic.Next()); - } - - [Test] - public void Previous_Without_Sibling() - { - var asDynamic = GetDynamicNode(1173); - - Assert.IsNull(asDynamic.Previous()); - } - - [Test] - public void Previous() - { - var asDynamic = GetDynamicNode(1176); - - var result = asDynamic.Previous(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int)1178, (int)result.Id); - } - } - - /// - /// Extension methods used in tests - /// - public static class TestExtensionMethods - { - public static bool ContainsValue(this string s, int val) - { - return s.Contains(val.ToString()); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicNodeTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicNodeTests.cs deleted file mode 100644 index b000466a85..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicNodeTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.IO; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; -using umbraco.MacroEngines; -using umbraco.NodeFactory; -using System.Linq; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public class DynamicNodeTests : DynamicDocumentTestsBase - { - /// - /// We only need a new schema per fixture... speeds up testing - /// - protected override DatabaseBehavior DatabaseTestBehavior - { - get { return DatabaseBehavior.NewSchemaPerFixture; } - } - - public override void Initialize() - { - base.Initialize(); - //copy the umbraco settings file over - var currDir = new DirectoryInfo(TestHelper.CurrentAssemblyDirectory); - File.Copy( - currDir.Parent.Parent.Parent.GetDirectories("Umbraco.Web.UI") - .First() - .GetDirectories("config").First() - .GetFiles("umbracoSettings.Release.config").First().FullName, - Path.Combine(currDir.Parent.Parent.FullName, "config", "umbracoSettings.config"), - true); - - UmbracoSettings.SettingsFilePath = IOHelper.MapPath(SystemDirectories.Config + Path.DirectorySeparatorChar, false); - - //need to specify a custom callback for unit tests - DynamicNode.GetDataTypeCallback = (docTypeAlias, propertyAlias) => - { - if (propertyAlias == "content") - { - //return the rte type id - return Guid.Parse(Constants.PropertyEditors.TinyMCEv3); - } - return Guid.Empty; - }; - - } - - [Test] - [Ignore("This test will never work unless DynamicNode is refactored a lot in order to get a list of root nodes since root nodes don't have a parent to look up")] - public override void Is_First_Root_Nodes() - { - base.Is_First_Root_Nodes(); - } - - [Test] - [Ignore("This test will never work unless DynamicNode is refactored a lot in order to get a list of root nodes since root nodes don't have a parent to look up")] - public override void Is_Not_First_Root_Nodes() - { - base.Is_Not_First_Root_Nodes(); - } - - [Test] - [Ignore("This test will never work unless DynamicNode is refactored a lot in order to get a list of root nodes since root nodes don't have a parent to look up")] - public override void Is_Position_Root_Nodes() - { - base.Is_Position_Root_Nodes(); - } - - public override void TearDown() - { - base.TearDown(); - } - - protected override dynamic GetDynamicNode(int id) - { - //var template = Template.MakeNew("test", new User(0)); - //var ctx = GetUmbracoContext("/test", template.Id); - var ctx = GetUmbracoContext("/test", 1234); - - var cache = ctx.ContentCache.InnerCache as PublishedContentCache; - if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the legacy one is supported."); - - var node = new DynamicNode( - new DynamicBackingItem( - new Node(cache.GetXml(ctx).SelectSingleNode("//*[@id='" + id + "' and @isDoc]")))); - Assert.IsNotNull(node); - return (dynamic)node; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicPublishedContentCustomExtensionMethods.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicPublishedContentCustomExtensionMethods.cs deleted file mode 100644 index b2cebf3ed0..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicPublishedContentCustomExtensionMethods.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Web.Models; - -namespace Umbraco.Tests.PublishedContent -{ - public static class DynamicPublishedContentCustomExtensionMethods - { - - public static string DynamicDocumentNoParameters(this DynamicPublishedContent doc) - { - return "Hello world"; - } - - public static string DynamicDocumentCustomString(this DynamicPublishedContent doc, string custom) - { - return custom; - } - - public static string DynamicDocumentMultiParam(this DynamicPublishedContent doc, string custom, int i, bool b) - { - return custom + i + b; - } - - public static string DynamicDocumentListMultiParam(this DynamicPublishedContentList doc, string custom, int i, bool b) - { - return custom + i + b; - } - - public static string DynamicDocumentEnumerableMultiParam(this IEnumerable doc, string custom, int i, bool b) - { - return custom + i + b; - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicPublishedContentTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicPublishedContentTests.cs deleted file mode 100644 index fdf097472d..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicPublishedContentTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web; -using Umbraco.Web.Models; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public class DynamicPublishedContentTests : DynamicDocumentTestsBase - { - public override void Initialize() - { - base.Initialize(); - - } - - public override void TearDown() - { - base.TearDown(); - } - - internal DynamicPublishedContent GetNode(int id) - { - //var template = Template.MakeNew("test", new User(0)); - //var ctx = GetUmbracoContext("/test", template.Id); - var ctx = GetUmbracoContext("/test", 1234); - var doc = ctx.ContentCache.GetById(id); - Assert.IsNotNull(doc); - var dynamicNode = new DynamicPublishedContent(doc); - Assert.IsNotNull(dynamicNode); - return dynamicNode; - } - - protected override dynamic GetDynamicNode(int id) - { - return GetNode(id).AsDynamic(); - } - - [Test] - public void Custom_Extension_Methods() - { - var asDynamic = GetDynamicNode(1173); - - Assert.AreEqual("Hello world", asDynamic.DynamicDocumentNoParameters()); - Assert.AreEqual("Hello world!", asDynamic.DynamicDocumentCustomString("Hello world!")); - Assert.AreEqual("Hello world!" + 123 + false, asDynamic.DynamicDocumentMultiParam("Hello world!", 123, false)); - Assert.AreEqual("Hello world!" + 123 + false, asDynamic.Children.DynamicDocumentListMultiParam("Hello world!", 123, false)); - Assert.AreEqual("Hello world!" + 123 + false, asDynamic.Children.DynamicDocumentEnumerableMultiParam("Hello world!", 123, false)); - - } - - [Test] - public void Returns_IDocument_Object() - { - var helper = new TestHelper(GetNode(1173)); - var doc = helper.GetDoc(); - //HasProperty is only a prop on DynamicPublishedContent, NOT IPublishedContent - Assert.IsFalse(doc.GetType().GetProperties().Any(x => x.Name == "HasProperty")); - } - - [Test] - public void Returns_DynamicDocument_Object() - { - var helper = new TestHelper(GetNode(1173)); - var doc = helper.GetDocAsDynamic(); - //HasProperty is only a prop on DynamicPublishedContent, NOT IPublishedContent - Assert.IsTrue(doc.HasProperty(Constants.Conventions.Content.UrlAlias)); - } - - [Test] - public void Returns_DynamicDocument_Object_After_Casting() - { - var helper = new TestHelper(GetNode(1173)); - var doc = helper.GetDoc(); - var ddoc = (dynamic) doc; - //HasProperty is only a prop on DynamicPublishedContent, NOT IPublishedContent - Assert.IsTrue(ddoc.HasProperty(Constants.Conventions.Content.UrlAlias)); - } - - /// - /// Test class to mimic UmbracoHelper when returning docs - /// - public class TestHelper - { - private readonly DynamicPublishedContent _doc; - - public TestHelper(DynamicPublishedContent doc) - { - _doc = doc; - } - - public IPublishedContent GetDoc() - { - return _doc; - } - - public dynamic GetDocAsDynamic() - { - return _doc.AsDynamic(); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicXmlConverterTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicXmlConverterTests.cs deleted file mode 100644 index f0ad32a1bc..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicXmlConverterTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Xml; -using System.Xml.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Dynamics; -using Umbraco.Tests.PartialTrust; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public class DynamicXmlConverterTests : AbstractPartialTrustFixture - { - [Test] - public void Convert_To_Raw_String() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml( - XmlHelper.StripDashesInElementOrAttributeNames(xml), - xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.Value); - } - - [Test] - public void Convert_To_Raw_XElement() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml( - XmlHelper.StripDashesInElementOrAttributeNames(xml), - xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.Value.ToString(SaveOptions.DisableFormatting)); - } - - [Test] - public void Convert_To_Raw_XmlElement() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml( - XmlHelper.StripDashesInElementOrAttributeNames(xml), - xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.Value.OuterXml); - } - - [Test] - public void Convert_To_Raw_XmlDocument() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml( - XmlHelper.StripDashesInElementOrAttributeNames(xml), - xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.Value.InnerXml); - } - - [Test] - public void Convert_To_String() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml(xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result); - } - - [Test] - public void Convert_To_XElement() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml(xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.ToString(SaveOptions.DisableFormatting)); - } - - [Test] - public void Convert_To_XmlElement() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml(xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.OuterXml); - } - - [Test] - public void Convert_To_XmlDocument() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var dXml = new DynamicXml(xml); - var result = dXml.TryConvertTo(); - Assert.IsTrue(result.Success); - Assert.AreEqual(xml, result.Result.InnerXml); - } - - public override void TestSetup() - { - - } - - public override void TestTearDown() - { - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicXmlTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicXmlTests.cs deleted file mode 100644 index 642952e4aa..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/DynamicXmlTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Diagnostics; -using Microsoft.CSharp.RuntimeBinder; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Dynamics; -using System.Linq; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public class DynamicXmlTests - { - /// - /// Ensures that when we return the xml structure we get the real structure, not the replaced hyphen structure - /// see: http://issues.umbraco.org/issue/U4-1405#comment=67-5113 - /// http://issues.umbraco.org/issue/U4-1636 - /// - [Test] - public void Deals_With_Hyphenated_Values() - { - var xml = @" - - True - 1161 - /content/ - 12 december Zorgbeurs Care - -"; - - var typedXml = new DynamicXml( - XmlHelper.StripDashesInElementOrAttributeNames(xml), - xml); - dynamic dynamicXml = typedXml; - - var typedElement = typedXml.RawXmlElement.Element("url-picker"); - var dynamicElementByCleanedName = dynamicXml.urlpicker; - - Assert.IsNotNull(typedElement); - Assert.IsNotNull(dynamicElementByCleanedName); - - Assert.AreEqual( - typedElement.Attribute("some-attribute").Value, - dynamicElementByCleanedName.someattribute); - } - - [Test] - public void Custom_Extension_Method_Legacy() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var typedXml = new global::umbraco.MacroEngines.DynamicXml(xml); - dynamic dynamicXml = typedXml; - - //we haven't explicitly defined ElementAt so this will dynamically invoke this method - var element = dynamicXml.ElementAt(0); - - Assert.AreEqual("1057", Enumerable.First(element.BaseElement.Elements()).Attribute("id").Value); - } - - [Test] - public void Custom_Extension_Method() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var typedXml = new DynamicXml(xml); - - dynamic dynamicXml = typedXml; - - //we haven't explicitly defined ElementAt so this will dynamically invoke this method - var element = dynamicXml.ElementAt(0); - - Assert.AreEqual("1057", Enumerable.First(element.BaseElement.Elements()).Attribute("id").Value); - } - - [Test] - public void Take_Legacy() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var typedXml = new global::umbraco.MacroEngines.DynamicXml(xml); - dynamic dynamicXml = typedXml; - var typedTaken = typedXml.Take(1); - var dynamicTaken = dynamicXml.Take(1); - - Assert.AreEqual(1, typedTaken.Count()); - Assert.AreEqual(1, Enumerable.Count(dynamicTaken)); - - Assert.AreEqual("1057", typedTaken.ElementAt(0).BaseElement.Elements().First().Attribute("id").Value); - Assert.AreEqual("1057", Enumerable.First(Enumerable.ElementAt(dynamicTaken, 0).BaseElement.Elements()).Attribute("id").Value); - } - - [Test] - public void Take() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var typedXml = new DynamicXml(xml); - dynamic dynamicXml = typedXml; - var typedTaken = typedXml.Take(1); - var dynamicTaken = dynamicXml.Take(1); - - Assert.AreEqual(1, typedTaken.Count()); - Assert.AreEqual(1, Enumerable.Count(dynamicTaken)); - - Assert.AreEqual("1057", typedTaken.ElementAt(0).BaseElement.Elements().First().Attribute("id").Value); - Assert.AreEqual("1057", Enumerable.First(Enumerable.ElementAt(dynamicTaken, 0).BaseElement.Elements()).Attribute("id").Value); - } - - [Test] - public void Ensure_Legacy_Objects_Are_Returned() - { - var xml = "/media/54/tulips.jpg1024768620888jpg/media/41/hydrangeas.jpg1024768595284jpg"; - var mediaItems = new global::umbraco.MacroEngines.DynamicXml(xml); - //Debug.WriteLine("full xml = {0}", mediaItems.ToXml()); - - if (mediaItems.Count() != 0) - { - foreach (dynamic item in mediaItems) - { - Type itemType = item.GetType(); - Debug.WriteLine("item type = {0}", itemType); - dynamic image = item.Image; - - Type imageType = image.GetType(); - Debug.WriteLine("image type = {0}", imageType); - - //ensure they are the same - Assert.AreEqual(itemType, imageType); - - //ensure they are legacy - Assert.AreEqual(typeof(global::umbraco.MacroEngines.DynamicXml), itemType); - Assert.AreEqual(typeof(global::umbraco.MacroEngines.DynamicXml), imageType); - } - } - } - - /// - /// Test the current Core class - /// - [Test] - public void Find_Test_Core_Class() - { - RunFindTest(x => new DynamicXml(x)); - } - - /// - /// Tests the macroEngines legacy class - /// - [Test] - public void Find_Test_Legacy_Class() - { - RunFindTest(x => new global::umbraco.MacroEngines.DynamicXml(x)); - } - - private void RunFindTest(Func getDynamicXml) - { - var xmlstring = @" - - - -"; - - dynamic dXml = getDynamicXml(xmlstring); - - var result1 = dXml.Find("@name", "test 1"); - var result2 = dXml.Find("@name", "test 2"); - var result3 = dXml.Find("@name", "test 3"); - var result4 = dXml.Find("@name", "dont find"); - - Assert.AreEqual("found 1", result1.value); - Assert.AreEqual("found 2", result2.value); - Assert.AreEqual("found 3", result3.value); - Assert.Throws(() => - { - //this will throw because result4 is not found - var temp = result4.value; - }); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/LegacyExamineBackedMediaTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/LegacyExamineBackedMediaTests.cs deleted file mode 100644 index 19f3046211..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/LegacyExamineBackedMediaTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Lucene.Net.Documents; -using Lucene.Net.Store; -using NUnit.Framework; -using Umbraco.Core.Configuration; -using Umbraco.Tests.UmbracoExamine; -using umbraco.MacroEngines; - -namespace Umbraco.Tests.PublishedContent -{ - public class LegacyExamineBackedMediaTests : ExamineBaseTest - { - public override void TestSetup() - { - base.TestSetup(); - UmbracoSettings.ForceSafeAliases = true; - UmbracoSettings.UmbracoLibraryCacheDuration = 1800; - UmbracoSettings.ForceSafeAliases = true; - } - - public override void TestTearDown() - { - base.TestTearDown(); - } - - [Test] - public void Ensure_Children_Are_Sorted() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var result = searcher.Search(searcher.CreateSearchCriteria().Id(1111).Compile()); - Assert.IsNotNull(result); - Assert.AreEqual(1, result.TotalItemCount); - - var searchItem = result.First(); - var backedMedia = new ExamineBackedMedia(searchItem, indexer, searcher); - var children = backedMedia.ChildrenAsList.Value; - - var currSort = 0; - for (var i = 0; i < children.Count(); i++) - { - Assert.GreaterOrEqual(children[i].SortOrder, currSort); - currSort = children[i].SortOrder; - } - } - - } - - [Test] - public void Ensure_Result_Has_All_Values() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var result = searcher.Search(searcher.CreateSearchCriteria().Id(1111).Compile()); - Assert.IsNotNull(result); - Assert.AreEqual(1, result.TotalItemCount); - - var searchItem = result.First(); - var backedMedia = new ExamineBackedMedia(searchItem, indexer, searcher); - - Assert.AreEqual(searchItem.Id, backedMedia.Id); - Assert.AreEqual(searchItem.Fields["sortOrder"], backedMedia.SortOrder.ToString()); - Assert.AreEqual(searchItem.Fields["urlName"], backedMedia.UrlName); - Assert.AreEqual(DateTools.StringToDate(searchItem.Fields["createDate"]), backedMedia.CreateDate); - Assert.AreEqual(DateTools.StringToDate(searchItem.Fields["updateDate"]), backedMedia.UpdateDate); - Assert.AreEqual(Guid.Parse(searchItem.Fields["version"]), backedMedia.Version); - Assert.AreEqual(searchItem.Fields["level"], backedMedia.Level.ToString()); - Assert.AreEqual(searchItem.Fields["writerID"], backedMedia.WriterID.ToString()); - Assert.AreEqual(searchItem.Fields["writerID"], backedMedia.CreatorID.ToString()); //there's only writerId in the xml - Assert.AreEqual(searchItem.Fields["writerName"], backedMedia.CreatorName); - Assert.AreEqual(searchItem.Fields["writerName"], backedMedia.WriterName); //tehre's only writer name in the xml - } - - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentDataTableTests.cs deleted file mode 100644 index c6ce668a40..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentDataTableTests.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Dynamics; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; - -namespace Umbraco.Tests.PublishedContent -{ - /// - /// Unit tests for IPublishedContent and extensions - /// - [TestFixture] - public class PublishedContentDataTableTests : BaseRoutingTest - { - public override void Initialize() - { - base.Initialize(); - //need to specify a different callback for testing - Umbraco.Web.PublishedContentExtensions.GetPropertyAliasesAndNames = s => - { - var userFields = new Dictionary() - { - {"property1", "Property 1"}, - {"property2", "Property 2"} - }; - if (s == "Child") - { - userFields.Add("property4", "Property 4"); - } - else - { - userFields.Add("property3", "Property 3"); - } - - //ensure the standard fields are there - var allFields = new Dictionary() - { - {"Id", "Id"}, - {"NodeName", "NodeName"}, - {"NodeTypeAlias", "NodeTypeAlias"}, - {"CreateDate", "CreateDate"}, - {"UpdateDate", "UpdateDate"}, - {"CreatorName", "CreatorName"}, - {"WriterName", "WriterName"}, - {"Url", "Url"} - }; - foreach (var f in userFields.Where(f => !allFields.ContainsKey(f.Key))) - { - allFields.Add(f.Key, f.Value); - } - return allFields; - }; - var routingContext = GetRoutingContext("/test"); - - //set the UmbracoContext.Current since the extension methods rely on it - UmbracoContext.Current = routingContext.UmbracoContext; - } - - public override void TearDown() - { - base.TearDown(); - Umbraco.Web.PublishedContentExtensions.GetPropertyAliasesAndNames = null; - UmbracoContext.Current = null; - } - - [Test] - public void To_DataTable() - { - var doc = GetContent(true, 1); - var dt = doc.ChildrenAsTable(); - - Assert.AreEqual(11, dt.Columns.Count); - Assert.AreEqual(3, dt.Rows.Count); - Assert.AreEqual("value4", dt.Rows[0]["Property 1"]); - Assert.AreEqual("value5", dt.Rows[0]["Property 2"]); - Assert.AreEqual("value6", dt.Rows[0]["Property 4"]); - Assert.AreEqual("value7", dt.Rows[1]["Property 1"]); - Assert.AreEqual("value8", dt.Rows[1]["Property 2"]); - Assert.AreEqual("value9", dt.Rows[1]["Property 4"]); - Assert.AreEqual("value10", dt.Rows[2]["Property 1"]); - Assert.AreEqual("value11", dt.Rows[2]["Property 2"]); - Assert.AreEqual("value12", dt.Rows[2]["Property 4"]); - } - - [Test] - public void To_DataTable_With_Filter() - { - var doc = GetContent(true, 1); - //change a doc type alias - ((TestPublishedContent) doc.Children.ElementAt(0)).DocumentTypeAlias = "DontMatch"; - - var dt = doc.ChildrenAsTable("Child"); - - Assert.AreEqual(11, dt.Columns.Count); - Assert.AreEqual(2, dt.Rows.Count); - Assert.AreEqual("value7", dt.Rows[0]["Property 1"]); - Assert.AreEqual("value8", dt.Rows[0]["Property 2"]); - Assert.AreEqual("value9", dt.Rows[0]["Property 4"]); - Assert.AreEqual("value10", dt.Rows[1]["Property 1"]); - Assert.AreEqual("value11", dt.Rows[1]["Property 2"]); - Assert.AreEqual("value12", dt.Rows[1]["Property 4"]); - } - - [Test] - public void To_DataTable_No_Rows() - { - var doc = GetContent(false, 1); - var dt = doc.ChildrenAsTable(); - //will return an empty data table - Assert.AreEqual(0, dt.Columns.Count); - Assert.AreEqual(0, dt.Rows.Count); - } - - private IPublishedContent GetContent(bool createChildren, int indexVals) - { - var d = new TestPublishedContent - { - CreateDate = DateTime.Now, - CreatorId = 1, - CreatorName = "Shannon", - DocumentTypeAlias = createChildren? "Parent" : "Child", - DocumentTypeId = 2, - Id = 3, - SortOrder = 4, - TemplateId = 5, - UpdateDate = DateTime.Now, - Path = "-1,3", - UrlName = "home-page", - Name = "Page" + Guid.NewGuid().ToString(), - Version = Guid.NewGuid(), - WriterId = 1, - WriterName = "Shannon", - Parent = null, - Level = 1, - Properties = new Collection( - new List() - { - new PropertyResult("property1", "value" + indexVals, Guid.NewGuid(), PropertyResultType.UserProperty), - new PropertyResult("property2", "value" + (indexVals + 1), Guid.NewGuid(), PropertyResultType.UserProperty) - }), - Children = new List() - }; - if (createChildren) - { - d.Children = new List() - { - GetContent(false, indexVals + 3), - GetContent(false, indexVals + 6), - GetContent(false, indexVals + 9) - }; - } - if (!createChildren) - { - //create additional columns, used to test the different columns for child nodes - d.Properties.Add(new PropertyResult("property4", "value" + (indexVals + 2), Guid.NewGuid(), PropertyResultType.UserProperty)); - } - else - { - d.Properties.Add(new PropertyResult("property3", "value" + (indexVals + 2), Guid.NewGuid(), PropertyResultType.UserProperty)); - } - return d; - } - - - private class TestPublishedContent : IPublishedContent - { - public string Url { get; set; } - public PublishedItemType ItemType { get; set; } - - IPublishedContent IPublishedContent.Parent - { - get { return Parent; } - } - IEnumerable IPublishedContent.Children - { - get { return Children; } - } - public IPublishedContent Parent { get; set; } - public int Id { get; set; } - public int TemplateId { get; set; } - public int SortOrder { get; set; } - public string Name { get; set; } - public string UrlName { get; set; } - public string DocumentTypeAlias { get; set; } - public int DocumentTypeId { get; set; } - public string WriterName { get; set; } - public string CreatorName { get; set; } - public int WriterId { get; set; } - public int CreatorId { get; set; } - public string Path { get; set; } - public DateTime CreateDate { get; set; } - public DateTime UpdateDate { get; set; } - public Guid Version { get; set; } - public int Level { get; set; } - public ICollection Properties { get; set; } - - public object this[string propertyAlias] - { - get { return GetProperty(propertyAlias).Value; } - } - - public IEnumerable Children { get; set; } - public IPublishedContentProperty GetProperty(string alias) - { - return Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentTestBase.cs deleted file mode 100644 index 4db386e3a4..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentTestBase.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.IO; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.PropertyEditors; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace Umbraco.Tests.PublishedContent -{ - /// - /// Abstract base class for tests for published content and published media - /// - public abstract class PublishedContentTestBase : BaseRoutingTest - { - public override void Initialize() - { - base.Initialize(); - - UmbracoSettings.SettingsFilePath = Core.IO.IOHelper.MapPath(Core.IO.SystemDirectories.Config + Path.DirectorySeparatorChar, false); - - //need to specify a custom callback for unit tests - PublishedContentHelper.GetDataTypeCallback = (docTypeAlias, propertyAlias) => - { - if (propertyAlias == "content") - { - //return the rte type id - return Guid.Parse(Constants.PropertyEditors.TinyMCEv3); - } - return Guid.Empty; - }; - - var rCtx = GetRoutingContext("/test", 1234); - UmbracoContext.Current = rCtx.UmbracoContext; - - } - - protected override void FreezeResolution() - { - PropertyEditorValueConvertersResolver.Current = new PropertyEditorValueConvertersResolver( - new[] - { - typeof(DatePickerPropertyEditorValueConverter), - typeof(TinyMcePropertyEditorValueConverter), - typeof(YesNoPropertyEditorValueConverter) - }); - - PublishedContentCacheResolver.Current = new PublishedContentCacheResolver(new PublishedContentCache()); - PublishedMediaCacheResolver.Current = new PublishedMediaCacheResolver(new PublishedMediaCache()); - - base.FreezeResolution(); - } - - public override void TearDown() - { - base.TearDown(); - - UmbracoContext.Current = null; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentTests.cs deleted file mode 100644 index 79940a82f2..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedContentTests.cs +++ /dev/null @@ -1,491 +0,0 @@ -using System.Linq; -using System.Web; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; - -namespace Umbraco.Tests.PublishedContent -{ - /// - /// Tests the methods on IPublishedContent using the DefaultPublishedContentStore - /// - [TestFixture] - public class PublishedContentTests : PublishedContentTestBase - { - protected override DatabaseBehavior DatabaseTestBehavior - { - get { return DatabaseBehavior.NoDatabasePerFixture; } - } - - protected override string GetXmlContent(int templateId) - { - return @" - - - - -]> - - - - - 1 - - - This is some content]]> - - - - - - - - - - - - - 1 - - - - - - - - - -"; - } - - internal IPublishedContent GetNode(int id) - { - var ctx = GetUmbracoContext("/test", 1234); - var doc = ctx.ContentCache.GetById(id); - Assert.IsNotNull(doc); - return doc; - } - - [Test] - public void Is_Last_From_Where_Filter_Dynamic_Linq() - { - var doc = GetNode(1173); - - foreach (var d in doc.Children.Where("Visible")) - { - if (d.Id != 1178) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - } - - [Test] - public void Is_Last_From_Where_Filter() - { - var doc = GetNode(1173); - - foreach (var d in doc.Children.Where(x => x.IsVisible())) - { - if (d.Id != 1178) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - } - - [Test] - public void Is_Last_From_Take() - { - var doc = GetNode(1173); - - foreach (var d in doc.Children.Take(3)) - { - if (d.Id != 1178) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - } - - [Test] - public void Is_Last_From_Skip() - { - var doc = GetNode(1173); - - foreach (var d in doc.Children.Skip(1)) - { - if (d.Id != 1176) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - } - - [Test] - public void Is_Last_From_Concat() - { - var doc = GetNode(1173); - - - foreach (var d in doc.Children.Concat(new[] { GetNode(1175), GetNode(4444) })) - { - if (d.Id != 4444) - { - Assert.IsFalse(d.IsLast()); - } - else - { - Assert.IsTrue(d.IsLast()); - } - } - } - - [Test] - public void Descendants_Ordered_Properly() - { - var doc = GetNode(1046); - - var currentLevel = 0; - var lastSortOrder = 0; - var levelChangesAt = new[] { 1046, 1173, 1174 }; - - foreach (var d in doc.DescendantsOrSelf()) - { - if (levelChangesAt.Contains(d.Id)) - { - Assert.Greater(d.Level, currentLevel); - currentLevel = d.Level; - } - else - { - Assert.AreEqual(currentLevel, d.Level); - Assert.Greater(d.SortOrder, lastSortOrder); - } - lastSortOrder = d.SortOrder; - } - } - - [Test] - public void Test_Get_Recursive_Val() - { - var doc = GetNode(1174); - var rVal = doc.GetRecursiveValue("testRecursive"); - var nullVal = doc.GetRecursiveValue("DoNotFindThis"); - Assert.AreEqual("This is the recursive val", rVal); - Assert.AreEqual("", nullVal); - } - - [Test] - public void Get_Property_Value_Uses_Converter() - { - var doc = GetNode(1173); - - var propVal = doc.GetPropertyValue("content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal.GetType())); - Assert.AreEqual("
This is some content
", propVal.ToString()); - - var propVal2 = doc.GetPropertyValue("content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal2.GetType())); - Assert.AreEqual("
This is some content
", propVal2.ToString()); - } - - [Test] - public void Complex_Linq() - { - var doc = GetNode(1173); - - var result = doc.Ancestors().OrderBy(x => x.Level) - .Single() - .Descendants() - .FirstOrDefault(x => x.GetPropertyValue("selectedNodes", "").Split(',').Contains("1173")); - - Assert.IsNotNull(result); - } - - [Test] - public void Index() - { - var doc = GetNode(1173); - Assert.AreEqual(0, doc.Index()); - doc = GetNode(1176); - Assert.AreEqual(3, doc.Index()); - doc = GetNode(1177); - Assert.AreEqual(1, doc.Index()); - doc = GetNode(1178); - Assert.AreEqual(2, doc.Index()); - } - - [Test] - public void Is_First() - { - var doc = GetNode(1046); //test root nodes - Assert.IsTrue(doc.IsFirst()); - doc = GetNode(1172); - Assert.IsFalse(doc.IsFirst()); - doc = GetNode(1173); //test normal nodes - Assert.IsTrue(doc.IsFirst()); - doc = GetNode(1175); - Assert.IsFalse(doc.IsFirst()); - } - - [Test] - public void Is_Not_First() - { - var doc = GetNode(1046); //test root nodes - Assert.IsFalse(doc.IsNotFirst()); - doc = GetNode(1172); - Assert.IsTrue(doc.IsNotFirst()); - doc = GetNode(1173); //test normal nodes - Assert.IsFalse(doc.IsNotFirst()); - doc = GetNode(1175); - Assert.IsTrue(doc.IsNotFirst()); - } - - [Test] - public void Is_Position() - { - var doc = GetNode(1046); //test root nodes - Assert.IsTrue(doc.IsPosition(0)); - doc = GetNode(1172); - Assert.IsTrue(doc.IsPosition(1)); - doc = GetNode(1173); //test normal nodes - Assert.IsTrue(doc.IsPosition(0)); - doc = GetNode(1175); - Assert.IsTrue(doc.IsPosition(1)); - } - - [Test] - public void Children_GroupBy_DocumentTypeAlias() - { - var doc = GetNode(1046); - - var found1 = doc.Children.GroupBy("DocumentTypeAlias"); - - Assert.AreEqual(2, found1.Count()); - Assert.AreEqual(2, found1.Single(x => x.Key.ToString() == "Home").Count()); - Assert.AreEqual(1, found1.Single(x => x.Key.ToString() == "CustomDocument").Count()); - } - - [Test] - public void Children_Where_DocumentTypeAlias() - { - var doc = GetNode(1046); - - var found1 = doc.Children.Where("DocumentTypeAlias == \"CustomDocument\""); - var found2 = doc.Children.Where("DocumentTypeAlias == \"Home\""); - - Assert.AreEqual(1, found1.Count()); - Assert.AreEqual(2, found2.Count()); - } - - [Test] - public void Children_Order_By_Update_Date() - { - var doc = GetNode(1173); - - var ordered = doc.Children.OrderBy("UpdateDate"); - - var correctOrder = new[] { 1178, 1177, 1174, 1176 }; - for (var i = 0; i < correctOrder.Length; i++) - { - Assert.AreEqual(correctOrder[i], ordered.ElementAt(i).Id); - } - - } - - [Test] - public void HasProperty() - { - var doc = GetNode(1173); - - var hasProp = doc.HasProperty(Constants.Conventions.Content.UrlAlias); - - Assert.AreEqual(true, (bool)hasProp); - - } - - - [Test] - public void HasValue() - { - var doc = GetNode(1173); - - var hasValue = doc.HasValue(Constants.Conventions.Content.UrlAlias); - var noValue = doc.HasValue("blahblahblah"); - - Assert.IsTrue(hasValue); - Assert.IsFalse(noValue); - } - - - [Test] - public void Ancestors_Where_Visible() - { - var doc = GetNode(1174); - - var whereVisible = doc.Ancestors().Where("Visible"); - - Assert.AreEqual(1, whereVisible.Count()); - - } - - [Test] - public void Visible() - { - var hidden = GetNode(1046); - var visible = GetNode(1173); - - Assert.IsFalse(hidden.IsVisible()); - Assert.IsTrue(visible.IsVisible()); - } - - - [Test] - public void Ancestor_Or_Self() - { - var doc = GetNode(1173); - - var result = doc.AncestorOrSelf(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int)1046, (int)result.Id); - } - - [Test] - public void Ancestors_Or_Self() - { - var doc = GetNode(1174); - - var result = doc.AncestorsOrSelf(); - - Assert.IsNotNull(result); - - Assert.AreEqual(3, result.Count()); - Assert.IsTrue(result.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1174, 1173, 1046 })); - } - - [Test] - public void Ancestors() - { - var doc = GetNode(1174); - - var result = doc.Ancestors(); - - Assert.IsNotNull(result); - - Assert.AreEqual(2, result.Count()); - Assert.IsTrue(result.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1173, 1046 })); - } - - [Test] - public void Descendants_Or_Self() - { - var doc = GetNode(1046); - - var result = doc.DescendantsOrSelf(); - - Assert.IsNotNull(result); - - Assert.AreEqual(8, result.Count()); - Assert.IsTrue(result.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1046, 1173, 1174, 1176, 1175 })); - } - - [Test] - public void Descendants() - { - var doc = GetNode(1046); - - var result = doc.Descendants(); - - Assert.IsNotNull(result); - - Assert.AreEqual(7, result.Count()); - Assert.IsTrue(result.Select(x => ((dynamic)x).Id).ContainsAll(new dynamic[] { 1173, 1174, 1176, 1175, 4444 })); - } - - [Test] - public void Up() - { - var doc = GetNode(1173); - - var result = doc.Up(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int)1046, (int)result.Id); - } - - [Test] - public void Down() - { - var doc = GetNode(1173); - - var result = doc.Down(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int)1174, (int)result.Id); - } - - [Test] - public void Next() - { - var doc = GetNode(1173); - - var result = doc.Next(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int)1175, (int)result.Id); - } - - [Test] - public void Next_Without_Sibling() - { - var doc = GetNode(1176); - - Assert.IsNull(doc.Next()); - } - - [Test] - public void Previous_Without_Sibling() - { - var doc = GetNode(1173); - - Assert.IsNull(doc.Previous()); - } - - [Test] - public void Previous() - { - var doc = GetNode(1176); - - var result = doc.Previous(); - - Assert.IsNotNull(result); - - Assert.AreEqual((int)1178, (int)result.Id); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedMediaTests.cs deleted file mode 100644 index 36cba1ad3e..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/PublishedMediaTests.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using System.Xml.XPath; -using Examine; -using Examine.LuceneEngine; -using Examine.LuceneEngine.Providers; -using Lucene.Net.Analysis.Standard; -using Lucene.Net.Store; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.UmbracoExamine; -using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; -using UmbracoExamine; -using UmbracoExamine.DataServices; -using umbraco.BusinessLogic; -using System.Linq; - -namespace Umbraco.Tests.PublishedContent -{ - /// - /// Tests the typed extension methods on IPublishedContent using the DefaultPublishedMediaStore - /// - [TestFixture, RequiresSTA] - public class PublishedMediaTests : PublishedContentTestBase - { - - public override void Initialize() - { - base.Initialize(); - UmbracoExamineSearcher.DisableInitializationCheck = true; - BaseUmbracoIndexer.DisableInitializationCheck = true; - UmbracoSettings.ForceSafeAliases = true; - UmbracoSettings.UmbracoLibraryCacheDuration = 1800; - UmbracoSettings.ForceSafeAliases = true; - } - - public override void TearDown() - { - base.TearDown(); - UmbracoExamineSearcher.DisableInitializationCheck = null; - BaseUmbracoIndexer.DisableInitializationCheck = null; - } - - /// - /// Shared with PublishMediaStoreTests - /// - /// - /// - /// - internal static IPublishedContent GetNode(int id, UmbracoContext umbracoContext) - { - var ctx = umbracoContext; - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(), ctx); - var doc = cache.GetById(id); - Assert.IsNotNull(doc); - return doc; - } - - private IPublishedContent GetNode(int id) - { - return GetNode(id, GetUmbracoContext("/test", 1234)); - } - - [Test] - public void Ensure_Children_Sorted_With_Examine() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var ctx = GetUmbracoContext("/test", 1234); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace - var publishedMedia = cache.GetById(1111); - var rootChildren = publishedMedia.Children().ToArray(); - var currSort = 0; - for (var i = 0; i < rootChildren.Count(); i++) - { - Assert.GreaterOrEqual(rootChildren[i].SortOrder, currSort); - currSort = rootChildren[i].SortOrder; - } - } - - - - - - } - - - [Test] - public void Do_Not_Find_In_Recycle_Bin() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var ctx = GetUmbracoContext("/test", 1234); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //ensure it is found - var publishedMedia = cache.GetById(3113); - Assert.IsNotNull(publishedMedia); - - //move item to recycle bin - var newXml = XElement.Parse(@" - - 115 - 268 - 10726 - jpg - "); - indexer.ReIndexNode(newXml, "media"); - - //ensure it still exists in the index (raw examine search) - var criteria = searcher.CreateSearchCriteria(); - var filter = criteria.Id(3113); - var found = searcher.Search(filter.Compile()); - Assert.IsNotNull(found); - Assert.AreEqual(1, found.TotalItemCount); - - //ensure it does not show up in the published media store - var recycledMedia = cache.GetById(3113); - Assert.IsNull(recycledMedia); - - } - - } - - [Test] - public void Children_With_Examine() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var ctx = GetUmbracoContext("/test", 1234); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace - var publishedMedia = cache.GetById(1111); - var rootChildren = publishedMedia.Children(); - Assert.IsTrue(rootChildren.Select(x => x.Id).ContainsAll(new[] { 2222, 1113, 1114, 1115, 1116 })); - - var publishedChild1 = cache.GetById(2222); - var subChildren = publishedChild1.Children(); - Assert.IsTrue(subChildren.Select(x => x.Id).ContainsAll(new[] { 2112 })); - } - } - - [Test] - public void Descendants_With_Examine() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var ctx = GetUmbracoContext("/test", 1234); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace - var publishedMedia = cache.GetById(1111); - var rootDescendants = publishedMedia.Descendants(); - Assert.IsTrue(rootDescendants.Select(x => x.Id).ContainsAll(new[] { 2112, 2222, 1113, 1114, 1115, 1116 })); - - var publishedChild1 = cache.GetById(2222); - var subDescendants = publishedChild1.Descendants(); - Assert.IsTrue(subDescendants.Select(x => x.Id).ContainsAll(new[] { 2112, 3113 })); - } - } - - [Test] - public void DescendantsOrSelf_With_Examine() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var ctx = GetUmbracoContext("/test", 1234); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace - var publishedMedia = cache.GetById(1111); - var rootDescendants = publishedMedia.DescendantsOrSelf(); - Assert.IsTrue(rootDescendants.Select(x => x.Id).ContainsAll(new[] { 1111, 2112, 2222, 1113, 1114, 1115, 1116 })); - - var publishedChild1 = cache.GetById(2222); - var subDescendants = publishedChild1.DescendantsOrSelf(); - Assert.IsTrue(subDescendants.Select(x => x.Id).ContainsAll(new[] { 2222, 2112, 3113 })); - } - } - - [Test] - public void Ancestors_With_Examine() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var ctx = GetUmbracoContext("/test", 1234); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace - var publishedMedia = cache.GetById(3113); - var ancestors = publishedMedia.Ancestors(); - Assert.IsTrue(ancestors.Select(x => x.Id).ContainsAll(new[] { 2112, 2222, 1111 })); - } - - } - - [Test] - public void AncestorsOrSelf_With_Examine() - { - using (var luceneDir = new RAMDirectory()) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); - indexer.RebuildIndex(); - var ctx = GetUmbracoContext("/test", 1234); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(searcher, indexer), ctx); - - //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace - var publishedMedia = cache.GetById(3113); - var ancestors = publishedMedia.AncestorsOrSelf(); - Assert.IsTrue(ancestors.Select(x => x.Id).ContainsAll(new[] { 3113, 2112, 2222, 1111 })); - } - } - - [Test] - public void Children_Without_Examine() - { - var user = new User(0); - var mType = global::umbraco.cms.businesslogic.media.MediaType.MakeNew(user, "TestMediaType"); - var mRoot = global::umbraco.cms.businesslogic.media.Media.MakeNew("MediaRoot", mType, user, -1); - - var mChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child1", mType, user, mRoot.Id); - var mChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child2", mType, user, mRoot.Id); - var mChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child3", mType, user, mRoot.Id); - - var mSubChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild1", mType, user, mChild1.Id); - var mSubChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild2", mType, user, mChild1.Id); - var mSubChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild3", mType, user, mChild1.Id); - - var publishedMedia = GetNode(mRoot.Id); - var rootChildren = publishedMedia.Children(); - Assert.IsTrue(rootChildren.Select(x => x.Id).ContainsAll(new[] { mChild1.Id, mChild2.Id, mChild3.Id })); - - var publishedChild1 = GetNode(mChild1.Id); - var subChildren = publishedChild1.Children(); - Assert.IsTrue(subChildren.Select(x => x.Id).ContainsAll(new[] { mSubChild1.Id, mSubChild2.Id, mSubChild3.Id })); - } - - [Test] - public void Descendants_Without_Examine() - { - var user = new User(0); - var mType = global::umbraco.cms.businesslogic.media.MediaType.MakeNew(user, "TestMediaType"); - var mRoot = global::umbraco.cms.businesslogic.media.Media.MakeNew("MediaRoot", mType, user, -1); - - var mChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child1", mType, user, mRoot.Id); - var mChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child2", mType, user, mRoot.Id); - var mChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child3", mType, user, mRoot.Id); - - var mSubChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild1", mType, user, mChild1.Id); - var mSubChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild2", mType, user, mChild1.Id); - var mSubChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild3", mType, user, mChild1.Id); - - var publishedMedia = GetNode(mRoot.Id); - var rootDescendants = publishedMedia.Descendants(); - Assert.IsTrue(rootDescendants.Select(x => x.Id).ContainsAll(new[] { mChild1.Id, mChild2.Id, mChild3.Id, mSubChild1.Id, mSubChild2.Id, mSubChild3.Id })); - - var publishedChild1 = GetNode(mChild1.Id); - var subDescendants = publishedChild1.Descendants(); - Assert.IsTrue(subDescendants.Select(x => x.Id).ContainsAll(new[] { mSubChild1.Id, mSubChild2.Id, mSubChild3.Id })); - } - - [Test] - public void DescendantsOrSelf_Without_Examine() - { - var user = new User(0); - var mType = global::umbraco.cms.businesslogic.media.MediaType.MakeNew(user, "TestMediaType"); - var mRoot = global::umbraco.cms.businesslogic.media.Media.MakeNew("MediaRoot", mType, user, -1); - - var mChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child1", mType, user, mRoot.Id); - var mChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child2", mType, user, mRoot.Id); - var mChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child3", mType, user, mRoot.Id); - - var mSubChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild1", mType, user, mChild1.Id); - var mSubChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild2", mType, user, mChild1.Id); - var mSubChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild3", mType, user, mChild1.Id); - - var publishedMedia = GetNode(mRoot.Id); - var rootDescendantsOrSelf = publishedMedia.DescendantsOrSelf(); - Assert.IsTrue(rootDescendantsOrSelf.Select(x => x.Id).ContainsAll( - new[] { mRoot.Id, mChild1.Id, mChild2.Id, mChild3.Id, mSubChild1.Id, mSubChild2.Id, mSubChild3.Id })); - - var publishedChild1 = GetNode(mChild1.Id); - var subDescendantsOrSelf = publishedChild1.DescendantsOrSelf(); - Assert.IsTrue(subDescendantsOrSelf.Select(x => x.Id).ContainsAll( - new[] { mChild1.Id, mSubChild1.Id, mSubChild2.Id, mSubChild3.Id })); - } - - [Test] - public void Parent_Without_Examine() - { - var user = new User(0); - var mType = global::umbraco.cms.businesslogic.media.MediaType.MakeNew(user, "TestMediaType"); - var mRoot = global::umbraco.cms.businesslogic.media.Media.MakeNew("MediaRoot", mType, user, -1); - - var mChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child1", mType, user, mRoot.Id); - var mChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child2", mType, user, mRoot.Id); - var mChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child3", mType, user, mRoot.Id); - - var mSubChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild1", mType, user, mChild1.Id); - var mSubChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild2", mType, user, mChild1.Id); - var mSubChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild3", mType, user, mChild1.Id); - - var publishedRoot = GetNode(mRoot.Id); - Assert.AreEqual(null, publishedRoot.Parent); - - var publishedChild1 = GetNode(mChild1.Id); - Assert.AreEqual(mRoot.Id, publishedChild1.Parent.Id); - - var publishedSubChild1 = GetNode(mSubChild1.Id); - Assert.AreEqual(mChild1.Id, publishedSubChild1.Parent.Id); - } - - - [Test] - public void Ancestors_Without_Examine() - { - var user = new User(0); - var mType = global::umbraco.cms.businesslogic.media.MediaType.MakeNew(user, "TestMediaType"); - var mRoot = global::umbraco.cms.businesslogic.media.Media.MakeNew("MediaRoot", mType, user, -1); - - var mChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child1", mType, user, mRoot.Id); - var mChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child2", mType, user, mRoot.Id); - var mChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child3", mType, user, mRoot.Id); - - var mSubChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild1", mType, user, mChild1.Id); - var mSubChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild2", mType, user, mChild1.Id); - var mSubChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild3", mType, user, mChild1.Id); - - var publishedSubChild1 = GetNode(mSubChild1.Id); - Assert.IsTrue(publishedSubChild1.Ancestors().Select(x => x.Id).ContainsAll(new[] { mChild1.Id, mRoot.Id })); - } - - [Test] - public void AncestorsOrSelf_Without_Examine() - { - var user = new User(0); - var mType = global::umbraco.cms.businesslogic.media.MediaType.MakeNew(user, "TestMediaType"); - var mRoot = global::umbraco.cms.businesslogic.media.Media.MakeNew("MediaRoot", mType, user, -1); - - var mChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child1", mType, user, mRoot.Id); - var mChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child2", mType, user, mRoot.Id); - var mChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("Child3", mType, user, mRoot.Id); - - var mSubChild1 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild1", mType, user, mChild1.Id); - var mSubChild2 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild2", mType, user, mChild1.Id); - var mSubChild3 = global::umbraco.cms.businesslogic.media.Media.MakeNew("SubChild3", mType, user, mChild1.Id); - - var publishedSubChild1 = GetNode(mSubChild1.Id); - Assert.IsTrue(publishedSubChild1.AncestorsOrSelf().Select(x => x.Id).ContainsAll( - new[] { mSubChild1.Id, mChild1.Id, mRoot.Id })); - } - } - - -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContent/StronglyTypedQueryTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContent/StronglyTypedQueryTests.cs deleted file mode 100644 index e437829956..0000000000 --- a/src/Umbraco.Tests/PublishedCache/PublishedContent/StronglyTypedQueryTests.cs +++ /dev/null @@ -1,432 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; -using Umbraco.Web.Models; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public class StronglyTypedQueryTests : PublishedContentTestBase - { - public override void Initialize() - { - base.Initialize(); - } - - public override void TearDown() - { - base.TearDown(); - } - - protected override DatabaseBehavior DatabaseTestBehavior - { - get { return DatabaseBehavior.NoDatabasePerFixture; } - } - - protected override string GetXmlContent(int templateId) - { - return @" - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - -"; - } - - internal IPublishedContent GetNode(int id) - { - var ctx = UmbracoContext.Current; - var doc = ctx.ContentCache.GetById(id); - Assert.IsNotNull(doc); - return doc; - } - - - [Test] - public void Type_Test() - { - var doc = GetNode(1); - var result = doc.NewsArticles(TraversalType.Descendants).ToArray(); - Assert.AreEqual("John doe", result[0].ArticleAuthor); - Assert.AreEqual("John Smith", result[1].ArticleAuthor); - } - - - [Test] - public void As_Test() - { - var doc = GetNode(1); - var result = doc.AsHome(); - Assert.AreEqual("Test site", result.SiteName); - - Assert.Throws(() => doc.AsContentPage()); - } - - } - - //NOTE: Some of these class will be moved in to the core once all this is working the way we want - - #region Gen classes & supporting classes - - //TOOD: SD: This class could be the way that the UmbracoHelper deals with looking things up in the background, we might not - // even expose it publicly but it could handle any caching (per request) that might be required when looking up any objects... - // though we might not need it at all, not sure yet. - // However, what we need to do is implement the GetDocumentsByType method of the IPublishedStore, see the TODO there. - // It might be nicer to have a QueryContext on the UmbracoHelper (we can still keep the Content and TypedContent, etc... - // methods, but these would just wrap the QueryContext attached to it. Other methods on the QueryContext will be - // ContentByType, TypedContentByType, etc... then we can also have extension methods like below for strongly typed - // access like: GetAllHomes, GetAllNewsArticles, etc... - - //public class QueryDataContext - //{ - // private readonly IPublishedContentStore _contentStore; - // private readonly UmbracoContext _umbracoContext; - - // internal QueryDataContext(IPublishedContentStore contentStore, UmbracoContext umbracoContext) - // { - // _contentStore = contentStore; - // _umbracoContext = umbracoContext; - // } - - // public IPublishedContent GetDocumentById(int id) - // { - // return _contentStore.GetDocumentById(_umbracoContext, id); - // } - - // public IEnumerable GetByDocumentType(string alias) - // { - - // } - //} - - public enum TraversalType - { - Children, - Ancestors, - AncestorsOrSelf, - Descendants, - DescendantsOrSelf - } - - public static class StronglyTypedQueryExtensions - { - private static IEnumerable GetEnumerable(this IPublishedContent content, string docTypeAlias, TraversalType traversalType = TraversalType.Children) - { - switch (traversalType) - { - case TraversalType.Children: - return content.Children.Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.Ancestors: - return content.Ancestors().Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.AncestorsOrSelf: - return content.AncestorsOrSelf().Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.Descendants: - return content.Descendants().Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.DescendantsOrSelf: - return content.DescendantsOrSelf().Where(x => x.DocumentTypeAlias == docTypeAlias); - default: - throw new ArgumentOutOfRangeException("traversalType"); - } - } - - private static T AsDocumentType(this IPublishedContent content, string alias, Func creator) - { - if (content.DocumentTypeAlias == alias) return creator(content); - throw new InvalidOperationException("The content type cannot be cast to " + typeof(T).FullName + " since it is type: " + content.DocumentTypeAlias); - } - - public static HomeContentItem AsHome(this IPublishedContent content) - { - return content.AsDocumentType("Home", x => new HomeContentItem(x)); - } - - public static IEnumerable Homes(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("Home", traversalType).Select(x => new HomeContentItem(x)); - } - - public static NewsArticleContentItem AsNewsArticle(this IPublishedContent content) - { - return content.AsDocumentType("NewsArticle", x => new NewsArticleContentItem(x)); - } - - public static IEnumerable NewsArticles(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("NewsArticle", traversalType).Select(x => new NewsArticleContentItem(x)); - } - - public static NewsLandingPageContentItem AsNewsLandingPage(this IPublishedContent content) - { - return content.AsDocumentType("NewsLandingPage", x => new NewsLandingPageContentItem(x)); - } - - public static IEnumerable NewsLandingPages(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("NewsLandingPage", traversalType).Select(x => new NewsLandingPageContentItem(x)); - } - - public static ContentPageContentItem AsContentPage(this IPublishedContent content) - { - return content.AsDocumentType("ContentPage", x => new ContentPageContentItem(x)); - } - - public static IEnumerable ContentPages(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("ContentPage", traversalType).Select(x => new ContentPageContentItem(x)); - } - } - - public class PublishedContentWrapper : IPublishedContent, IOwnerCollectionAware - { - protected IPublishedContent WrappedContent { get; private set; } - - public PublishedContentWrapper(IPublishedContent content) - { - WrappedContent = content; - } - - public string Url - { - get { return WrappedContent.Url; } - } - - public PublishedItemType ItemType - { - get { return WrappedContent.ItemType; } - } - - public IPublishedContent Parent - { - get { return WrappedContent.Parent; } - } - - public int Id - { - get { return WrappedContent.Id; } - } - public int TemplateId - { - get { return WrappedContent.TemplateId; } - } - public int SortOrder - { - get { return WrappedContent.SortOrder; } - } - public string Name - { - get { return WrappedContent.Name; } - } - public string UrlName - { - get { return WrappedContent.UrlName; } - } - public string DocumentTypeAlias - { - get { return WrappedContent.DocumentTypeAlias; } - } - public int DocumentTypeId - { - get { return WrappedContent.DocumentTypeId; } - } - public string WriterName - { - get { return WrappedContent.WriterName; } - } - public string CreatorName - { - get { return WrappedContent.CreatorName; } - } - public int WriterId - { - get { return WrappedContent.WriterId; } - } - public int CreatorId - { - get { return WrappedContent.CreatorId; } - } - public string Path - { - get { return WrappedContent.Path; } - } - public DateTime CreateDate - { - get { return WrappedContent.CreateDate; } - } - public DateTime UpdateDate - { - get { return WrappedContent.UpdateDate; } - } - public Guid Version - { - get { return WrappedContent.Version; } - } - public int Level - { - get { return WrappedContent.Level; } - } - public ICollection Properties - { - get { return WrappedContent.Properties; } - } - - public object this[string propertyAlias] - { - get { return GetProperty(propertyAlias).Value; } - } - - public IEnumerable Children - { - get { return WrappedContent.Children; } - } - public IPublishedContentProperty GetProperty(string alias) - { - return WrappedContent.GetProperty(alias); - } - - private IEnumerable _ownersCollection; - - /// - /// Need to get/set the owner collection when an item is returned from the result set of a query - /// - /// - /// Based on this issue here: http://issues.umbraco.org/issue/U4-1797 - /// - IEnumerable IOwnerCollectionAware.OwnersCollection - { - get - { - var publishedContentBase = WrappedContent as IOwnerCollectionAware; - if (publishedContentBase != null) - { - return publishedContentBase.OwnersCollection; - } - - //if the owners collection is null, we'll default to it's siblings - if (_ownersCollection == null) - { - //get the root docs if parent is null - _ownersCollection = this.Siblings(); - } - return _ownersCollection; - } - set - { - var publishedContentBase = WrappedContent as IOwnerCollectionAware; - if (publishedContentBase != null) - { - publishedContentBase.OwnersCollection = value; - } - else - { - _ownersCollection = value; - } - } - } - } - - public partial class HomeContentItem : ContentPageContentItem - { - public HomeContentItem(IPublishedContent content) - : base(content) - { - } - - public string SiteName - { - get { return WrappedContent.GetPropertyValue("siteName"); } - } - public string SiteDescription - { - get { return WrappedContent.GetPropertyValue("siteDescription"); } - } - } - - public partial class NewsLandingPageContentItem : ContentPageContentItem - { - public NewsLandingPageContentItem(IPublishedContent content) - : base(content) - { - } - - public string PageTitle - { - get { return WrappedContent.GetPropertyValue("pageTitle"); } - } - } - - public partial class NewsArticleContentItem : PublishedContentWrapper - { - public NewsArticleContentItem(IPublishedContent content) - : base(content) - { - } - - public string ArticleContent - { - get { return WrappedContent.GetPropertyValue("articleContent"); } - } - public DateTime ArticleDate - { - get { return WrappedContent.GetPropertyValue("articleDate"); } - } - public string ArticleAuthor - { - get { return WrappedContent.GetPropertyValue("articleAuthor"); } - } - } - - public partial class ContentPageContentItem : PublishedContentWrapper - { - public ContentPageContentItem(IPublishedContent content) - : base(content) - { - } - - public string BodyContent - { - get { return WrappedContent.GetPropertyValue("bodyContent"); } - } - } - - #endregion -} \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedContentCacheTests.cs index e91f73fccc..ac0465eea6 100644 --- a/src/Umbraco.Tests/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/PublishedCache/PublishedContentCacheTests.cs @@ -4,6 +4,9 @@ using System.Xml; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.PropertyEditors; using Umbraco.Tests.TestHelpers; using Umbraco.Web; using Umbraco.Web.PublishedCache; @@ -14,11 +17,12 @@ using umbraco.BusinessLogic; namespace Umbraco.Tests.PublishedCache { [TestFixture] - public class PublishContentCacheTests + public class PublishContentCacheTests : BaseWebTest { private FakeHttpContextFactory _httpContextFactory; private UmbracoContext _umbracoContext; private ContextualPublishedContentCache _cache; + private XmlDocument _xml; private string GetLegacyXml() { @@ -66,30 +70,23 @@ namespace Umbraco.Tests.PublishedCache } [SetUp] - public void SetUp() - { - TestHelper.SetupLog4NetForTests(); + public override void Initialize() + { + base.Initialize(); - //create the app context - ApplicationContext.Current = new ApplicationContext(CacheHelper.CreateDisabledCacheHelper()); - - _httpContextFactory = new FakeHttpContextFactory("~/Home"); - //ensure the StateHelper is using our custom context - StateHelper.HttpContext = _httpContextFactory.HttpContext; + _httpContextFactory = new FakeHttpContextFactory("~/Home"); + //ensure the StateHelper is using our custom context + StateHelper.HttpContext = _httpContextFactory.HttpContext; var settings = SettingsForTests.GetMockSettings(); var contentMock = Mock.Get(settings.Content); contentMock.Setup(x => x.UseLegacyXmlSchema).Returns(false); SettingsForTests.ConfigureSettings(settings); - + _xml = new XmlDocument(); + _xml.LoadXml(GetXml()); var cache = new PublishedContentCache { - GetXmlDelegate = (context, preview) => - { - var doc = new XmlDocument(); - doc.LoadXml(GetXml()); - return doc; - } + GetXmlDelegate = (context, preview) => _xml }; _umbracoContext = new UmbracoContext( @@ -99,33 +96,31 @@ namespace Umbraco.Tests.PublishedCache new WebSecurity(_httpContextFactory.HttpContext, ApplicationContext.Current)); _cache = _umbracoContext.ContentCache; - } + } - private void SetupForLegacy() + private void SetupForLegacy() { var settings = SettingsForTests.GetMockSettings(); var contentMock = Mock.Get(settings.Content); contentMock.Setup(x => x.UseLegacyXmlSchema).Returns(true); SettingsForTests.ConfigureSettings(settings); - - var cache = _umbracoContext.ContentCache.InnerCache as PublishedContentCache; - if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); - - cache.GetXmlDelegate = (context, preview) => - { - var doc = new XmlDocument(); - doc.LoadXml(GetLegacyXml()); - return doc; - }; + _xml = new XmlDocument(); + _xml.LoadXml(GetLegacyXml()); } + protected override void FreezeResolution() + { + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(); + base.FreezeResolution(); + } + [TearDown] public void TearDown() { - SettingsForTests.Reset(); + UmbracoSettings.Reset(); } - [Test] + [Test] public void Has_Content_LegacySchema() { SetupForLegacy(); diff --git a/src/Umbraco.Tests/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/PublishedCache/PublishedMediaCacheTests.cs index a1d922e86d..e8c03850a8 100644 --- a/src/Umbraco.Tests/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/PublishedCache/PublishedMediaCacheTests.cs @@ -6,6 +6,7 @@ using Examine; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Tests.PublishedContent; using Umbraco.Tests.TestHelpers; using Umbraco.Web; @@ -16,22 +17,15 @@ using umbraco.BusinessLogic; namespace Umbraco.Tests.PublishedCache { [TestFixture] - public class PublishMediaCacheTests : PublishedContentTestBase + public class PublishMediaCacheTests : BaseWebTest { - public override void Initialize() - { - base.Initialize(); - } - - - - - public override void TearDown() - { - base.TearDown(); - } - - [Test] + protected override void FreezeResolution() + { + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(); + base.FreezeResolution(); + } + + [Test] public void Get_Root_Docs() { var user = new User(0); @@ -249,11 +243,11 @@ namespace Umbraco.Tests.PublishedCache a => null, //we're not going to test this so ignore a => new List(), - (dd, a) => dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(a)), + (dd, a) => dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(a)), false), //callback to get the children d => children, - (dd, a) => dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(a)), + (dd, a) => dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(a)), false); return dicDoc; } diff --git a/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs b/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs index c5c4ff3891..65f240264a 100644 --- a/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs +++ b/src/Umbraco.Tests/PublishedContent/DynamicDocumentTestsBase.cs @@ -8,6 +8,8 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dynamics; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.PublishedContent @@ -29,7 +31,36 @@ namespace Umbraco.Tests.PublishedContent get { return DatabaseBehavior.NoDatabasePerFixture; } } - protected override string GetXmlContent(int templateId) + public override void Initialize() + { + // required so we can access property.Value + //PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver(); + + base.Initialize(); + + // need to specify a custom callback for unit tests + // AutoPublishedContentTypes generates properties automatically + // when they are requested, but we must declare those that we + // explicitely want to be here... + + var propertyTypes = new[] + { + // AutoPublishedContentType will auto-generate other properties + new PublishedPropertyType("umbracoNaviHide", 0, Guid.Empty), + new PublishedPropertyType("selectedNodes", 0, Guid.Empty), + new PublishedPropertyType("umbracoUrlAlias", 0, Guid.Empty), + new PublishedPropertyType("content", 0, Guid.Parse(Constants.PropertyEditors.TinyMCEv3)), + new PublishedPropertyType("testRecursive", 0, Guid.Empty), + new PublishedPropertyType("siteTitle", 0, Guid.Empty), + new PublishedPropertyType("creatorName", 0, Guid.Empty), + new PublishedPropertyType("blah", 0, Guid.Empty), // ugly error when that one is missing... + }; + var type = new AutoPublishedContentType(0, "anything", propertyTypes); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + + } + + protected override string GetXmlContent(int templateId) { return @" + + // need to specify a custom callback for unit tests + // AutoPublishedContentTypes generates properties automatically + var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] {}); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + + // need to specify a different callback for testing + PublishedContentExtensions.GetPropertyAliasesAndNames = s => { var userFields = new Dictionary() { @@ -118,12 +125,13 @@ namespace Umbraco.Tests.PublishedContent private IPublishedContent GetContent(bool createChildren, int indexVals) { + var contentTypeAlias = createChildren ? "Parent" : "Child"; var d = new TestPublishedContent { CreateDate = DateTime.Now, CreatorId = 1, CreatorName = "Shannon", - DocumentTypeAlias = createChildren? "Parent" : "Child", + DocumentTypeAlias = contentTypeAlias, DocumentTypeId = 2, Id = 3, SortOrder = 4, @@ -137,8 +145,8 @@ namespace Umbraco.Tests.PublishedContent WriterName = "Shannon", Parent = null, Level = 1, - Properties = new Collection( - new List() + Properties = new Collection( + new List() { new PropertyResult("property1", "value" + indexVals, PropertyResultType.UserProperty), new PropertyResult("property2", "value" + (indexVals + 1), PropertyResultType.UserProperty) @@ -166,50 +174,81 @@ namespace Umbraco.Tests.PublishedContent return d; } + // note - could probably rewrite those tests using SolidPublishedContentCache + // l8tr... + private class TestPublishedContent : IPublishedContent + { + public string Url { get; set; } + public PublishedItemType ItemType { get; set; } - private class TestPublishedContent : IPublishedContent - { - public string Url { get; set; } - public PublishedItemType ItemType { get; set; } + IPublishedContent IPublishedContent.Parent + { + get { return Parent; } + } - IPublishedContent IPublishedContent.Parent - { - get { return Parent; } - } - IEnumerable IPublishedContent.Children - { - get { return Children; } - } - public IPublishedContent Parent { get; set; } - public int Id { get; set; } - public int TemplateId { get; set; } - public int SortOrder { get; set; } - public string Name { get; set; } - public string UrlName { get; set; } - public string DocumentTypeAlias { get; set; } - public int DocumentTypeId { get; set; } - public string WriterName { get; set; } - public string CreatorName { get; set; } - public int WriterId { get; set; } - public int CreatorId { get; set; } - public string Path { get; set; } - public DateTime CreateDate { get; set; } - public DateTime UpdateDate { get; set; } - public Guid Version { get; set; } - public int Level { get; set; } - public ICollection Properties { get; set; } + IEnumerable IPublishedContent.Children + { + get { return Children; } + } - public object this[string propertyAlias] - { - get { return GetProperty(propertyAlias).Value; } - } + public IPublishedContent Parent { get; set; } + public int Id { get; set; } + public int TemplateId { get; set; } + public int SortOrder { get; set; } + public string Name { get; set; } + public string UrlName { get; set; } + public string DocumentTypeAlias { get; set; } + public int DocumentTypeId { get; set; } + public string WriterName { get; set; } + public string CreatorName { get; set; } + public int WriterId { get; set; } + public int CreatorId { get; set; } + public string Path { get; set; } + public DateTime CreateDate { get; set; } + public DateTime UpdateDate { get; set; } + public Guid Version { get; set; } + public int Level { get; set; } + public bool IsDraft { get; set; } + public int GetIndex() { throw new NotImplementedException();} + + public ICollection Properties { get; set; } - public IEnumerable Children { get; set; } - public IPublishedContentProperty GetProperty(string alias) - { - return Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - } + public object this[string propertyAlias] + { + get { return GetProperty(propertyAlias).ObjectValue; } + } + public IEnumerable Children { get; set; } + + public IPublishedProperty GetProperty(string alias) + { + return Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + public IPublishedProperty GetProperty(string alias, bool recurse) + { + var property = GetProperty(alias); + if (recurse == false) return property; + + IPublishedContent content = this; + while (content != null && (property == null || property.HasValue == false)) + { + content = content.Parent; + property = content == null ? null : content.GetProperty(alias); + } + + return property; + } + + public IEnumerable ContentSet + { + get { throw new NotImplementedException(); } + } + + public PublishedContentType ContentType + { + get { throw new NotImplementedException(); } + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs new file mode 100644 index 0000000000..c51613b8b8 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -0,0 +1,286 @@ +using System.Linq; +using System.Collections.ObjectModel; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.ObjectResolution; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web; +using Umbraco.Tests.TestHelpers; +using umbraco.BusinessLogic; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + public class PublishedContentMoreTests + { + // read http://stackoverflow.com/questions/7713326/extension-method-that-works-on-ienumerablet-and-iqueryablet + // and http://msmvps.com/blogs/jon_skeet/archive/2010/10/28/overloading-and-generic-constraints.aspx + // and http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx + + private PluginManager _pluginManager; + + [SetUp] + public void Setup() + { + // this is so the model factory looks into the test assembly + _pluginManager = PluginManager.Current; + PluginManager.Current = new PluginManager(false) + { + AssembliesToScan = _pluginManager.AssembliesToScan + .Union(new[] { typeof (PublishedContentMoreTests).Assembly}) + }; + + PropertyValueConvertersResolver.Current = + new PropertyValueConvertersResolver(); + PublishedContentModelFactoryResolver.Current = + new PublishedContentModelFactoryResolver(new PublishedContentModelFactoryImpl()); + Resolution.Freeze(); + + var caches = CreatePublishedContent(); + + ApplicationContext.Current = new ApplicationContext(false) { IsReady = true }; + var factory = new FakeHttpContextFactory("http://umbraco.local/"); + StateHelper.HttpContext = factory.HttpContext; + var context = new UmbracoContext( + factory.HttpContext, + ApplicationContext.Current, + caches); + UmbracoContext.Current = context; + } + + [TearDown] + public void TearDown() + { + PluginManager.Current = _pluginManager; + ApplicationContext.Current.DisposeIfDisposable(); + ApplicationContext.Current = null; + } + + [Test] + public void First() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + Assert.AreEqual("Content 1", content.Name); + } + + [Test] + public void DefaultContentSetIsSiblings() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + Assert.AreEqual(0, content.Index()); + Assert.IsTrue(content.IsFirst()); + } + + [Test] + public void RunOnLatestContentSet() + { + // get first content + var content = UmbracoContext.Current.ContentCache.GetAtRoot().First(); + var id = content.Id; + Assert.IsTrue(content.IsFirst()); + + // reverse => should be last, but set has not changed => still first + content = UmbracoContext.Current.ContentCache.GetAtRoot().Reverse().First(x => x.Id == id); + Assert.IsTrue(content.IsFirst()); + Assert.IsFalse(content.IsLast()); + + // reverse + new set => now it's last + content = UmbracoContext.Current.ContentCache.GetAtRoot().Reverse().ToContentSet().First(x => x.Id == id); + Assert.IsFalse(content.IsFirst()); + Assert.IsTrue(content.IsLast()); + + // reverse that set => should be first, but no new set => still last + content = UmbracoContext.Current.ContentCache.GetAtRoot().Reverse().ToContentSet().Reverse().First(x => x.Id == id); + Assert.IsFalse(content.IsFirst()); + Assert.IsTrue(content.IsLast()); + } + + [Test] + public void Distinct() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .Distinct() + .Distinct() + .ToContentSet() + .First(); + + Assert.AreEqual("Content 1", content.Name); + Assert.IsTrue(content.IsFirst()); + Assert.IsFalse(content.IsLast()); + + content = content.Next(); + Assert.AreEqual("Content 2", content.Name); + Assert.IsFalse(content.IsFirst()); + Assert.IsFalse(content.IsLast()); + + content = content.Next(); + Assert.AreEqual("Content 2Sub", content.Name); + Assert.IsFalse(content.IsFirst()); + Assert.IsTrue(content.IsLast()); + } + + [Test] + public void OfType1() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .Distinct() + .ToArray(); + Assert.AreEqual(2, content.Count()); + Assert.IsInstanceOf(content.First()); + var set = content.ToContentSet(); + Assert.IsInstanceOf(set.First()); + Assert.AreSame(set, set.First().ContentSet); + Assert.IsInstanceOf(set.First().Next()); + } + + [Test] + public void OfType2() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .Distinct() + .ToArray(); + Assert.AreEqual(1, content.Count()); + Assert.IsInstanceOf(content.First()); + var set = content.ToContentSet(); + Assert.IsInstanceOf(set.First()); + } + + [Test] + public void OfType() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .First(x => x.Prop1 == 1234); + Assert.AreEqual("Content 2", content.Name); + Assert.AreEqual(1234, content.Prop1); + } + + [Test] + public void Position() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .Where(x => x.GetPropertyValue("prop1") == 1234) + .ToContentSet() + .ToArray(); + + Assert.IsTrue(content.First().IsFirst()); + Assert.IsFalse(content.First().IsLast()); + Assert.IsFalse(content.First().Next().IsFirst()); + Assert.IsFalse(content.First().Next().IsLast()); + Assert.IsFalse(content.First().Next().Next().IsFirst()); + Assert.IsTrue(content.First().Next().Next().IsLast()); + } + + [Test] + public void Issue() + { + var content = UmbracoContext.Current.ContentCache.GetAtRoot() + .Distinct() + .OfType(); + + var where = content.Where(x => x.Prop1 == 1234); + var first = where.First(); + Assert.AreEqual(1234, first.Prop1); + + var content2 = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .First(x => x.Prop1 == 1234); + Assert.AreEqual(1234, content2.Prop1); + + var content3 = UmbracoContext.Current.ContentCache.GetAtRoot() + .OfType() + .First(); + Assert.AreEqual(1234, content3.Prop1); + } + + static SolidPublishedCaches CreatePublishedContent() + { + var caches = new SolidPublishedCaches(); + var cache = caches.ContentCache; + + var props = new[] + { + new PublishedPropertyType("prop1", 1, System.Guid.Empty), + }; + + var contentType1 = new PublishedContentType(1, "ContentType1", props); + var contentType2 = new PublishedContentType(2, "ContentType2", props); + var contentType2s = new PublishedContentType(3, "ContentType2Sub", props); + + cache.Add(new SolidPublishedContent(contentType1) + { + Id = 1, + SortOrder = 0, + Name = "Content 1", + UrlName = "content-1", + Path = "/1", + Level = 1, + Url = "/content-1", + ParentId = -1, + ChildIds = new int[] {}, + Properties = new Collection + { + new SolidPublishedProperty + { + PropertyTypeAlias = "prop1", + HasValue = true, + ObjectValue = 1234, + DataValue = "1234" + } + } + }); + + cache.Add(new SolidPublishedContent(contentType2) + { + Id = 2, + SortOrder = 1, + Name = "Content 2", + UrlName = "content-2", + Path = "/2", + Level = 1, + Url = "/content-2", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + PropertyTypeAlias = "prop1", + HasValue = true, + ObjectValue = 1234, + DataValue = "1234" + } + } + }); + + cache.Add(new SolidPublishedContent(contentType2s) + { + Id = 3, + SortOrder = 2, + Name = "Content 2Sub", + UrlName = "content-2sub", + Path = "/3", + Level = 1, + Url = "/content-2sub", + ParentId = -1, + ChildIds = new int[] { }, + Properties = new Collection + { + new SolidPublishedProperty + { + PropertyTypeAlias = "prop1", + HasValue = true, + ObjectValue = 1234, + DataValue = "1234" + } + } + }); + + return caches; + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index e93c4c092c..a5779cd988 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -1,8 +1,12 @@ using System; using System.IO; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Tests.TestHelpers; using Umbraco.Web; using Umbraco.Web.PublishedCache; @@ -19,16 +23,14 @@ namespace Umbraco.Tests.PublishedContent { base.Initialize(); - //need to specify a custom callback for unit tests - PublishedContentHelper.GetDataTypeCallback = (docTypeAlias, propertyAlias) => + // need to specify a custom callback for unit tests + var propertyTypes = new[] { - if (propertyAlias.InvariantEquals("content")) - { - //return the rte type id - return Constants.PropertyEditors.TinyMCEv3Alias; - } - return string.Empty; + // AutoPublishedContentType will auto-generate other properties + new PublishedPropertyType("content", 0, Guid.Parse(Constants.PropertyEditors.TinyMCEv3)), }; + var type = new AutoPublishedContentType(0, "anything", propertyTypes); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; var rCtx = GetRoutingContext("/test", 1234); UmbracoContext.Current = rCtx.UmbracoContext; @@ -40,14 +42,17 @@ namespace Umbraco.Tests.PublishedContent PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver( new[] { - typeof(DatePickerPropertyValueConverter), - typeof(TinyMcePropertyValueConverter), - typeof(YesNoPropertyValueConverter) + typeof(DatePickerValueConverter), + typeof(TinyMceValueConverter), + typeof(YesNoValueConverter) }); PublishedCachesResolver.Current = new PublishedCachesResolver(new PublishedCaches( new PublishedContentCache(), new PublishedMediaCache())); + if (PublishedContentModelFactoryResolver.HasCurrent == false) + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(); + base.FreezeResolution(); } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs new file mode 100644 index 0000000000..9ec15c66d5 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestElements.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Tests.PublishedContent +{ + class SolidPublishedCaches : IPublishedCaches + { + public readonly SolidPublishedContentCache ContentCache = new SolidPublishedContentCache(); + + public ContextualPublishedContentCache CreateContextualContentCache(UmbracoContext context) + { + return new ContextualPublishedContentCache(ContentCache, context); + } + + public ContextualPublishedMediaCache CreateContextualMediaCache(UmbracoContext context) + { + return null; + } + } + + class SolidPublishedContentCache : IPublishedContentCache + { + private readonly Dictionary _content = new Dictionary(); + + public void Add(SolidPublishedContent content) + { + _content[content.Id] = PublishedContentModelFactory.CreateModel(content); + } + + public void Clear() + { + _content.Clear(); + } + + public void ContentHasChanged(UmbracoContext umbracoContext) + { + throw new NotImplementedException(); + } + + public IPublishedContent GetByRoute(UmbracoContext umbracoContext, bool preview, string route, bool? hideTopLevelNode = null) + { + throw new NotImplementedException(); + } + + public string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId) + { + throw new NotImplementedException(); + } + + public IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int contentId) + { + return _content.ContainsKey(contentId) ? _content[contentId] : null; + } + + public IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) + { + return _content.Values.Where(x => x.Parent == null); + } + + public IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, Core.Xml.XPathVariable[] vars) + { + throw new NotImplementedException(); + } + + public IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, System.Xml.XPath.XPathExpression xpath, Core.Xml.XPathVariable[] vars) + { + throw new NotImplementedException(); + } + + public IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, Core.Xml.XPathVariable[] vars) + { + throw new NotImplementedException(); + } + + public IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, System.Xml.XPath.XPathExpression xpath, Core.Xml.XPathVariable[] vars) + { + throw new NotImplementedException(); + } + + public System.Xml.XPath.XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview) + { + throw new NotImplementedException(); + } + + public bool XPathNavigatorIsNavigable + { + get { throw new NotImplementedException(); } + } + + public bool HasContent(UmbracoContext umbracoContext, bool preview) + { + return _content.Count > 0; + } + } + + class SolidPublishedContent : IPublishedContent + { + #region Constructor + + public SolidPublishedContent(PublishedContentType contentType) + { + // initialize boring stuff + TemplateId = 0; + WriterName = CreatorName = string.Empty; + WriterId = CreatorId = 0; + CreateDate = UpdateDate = DateTime.Now; + Version = Guid.Empty; + IsDraft = false; + + ContentType = contentType; + DocumentTypeAlias = contentType.Alias; + DocumentTypeId = contentType.Id; + } + + #endregion + + #region Content + + public int Id { get; set; } + public int TemplateId { get; set; } + public int SortOrder { get; set; } + public string Name { get; set; } + public string UrlName { get; set; } + public string DocumentTypeAlias { get; private set; } + public int DocumentTypeId { get; private set; } + public string WriterName { get; set; } + public string CreatorName { get; set; } + public int WriterId { get; set; } + public int CreatorId { get; set; } + public string Path { get; set; } + public DateTime CreateDate { get; set; } + public DateTime UpdateDate { get; set; } + public Guid Version { get; set; } + public int Level { get; set; } + public string Url { get; set; } + + public PublishedItemType ItemType { get { return PublishedItemType.Content; } } + public bool IsDraft { get; set; } + + public int GetIndex() + { + var index = this.Siblings().FindIndex(x => x.Id == Id); + if (index < 0) + throw new IndexOutOfRangeException("Failed to find content in its siblings collection?!"); + return index; + } + + #endregion + + #region Tree + + public int ParentId { get; set; } + public IEnumerable ChildIds { get; set; } + + public IPublishedContent Parent { get { return UmbracoContext.Current.ContentCache.GetById(ParentId); } } + public IEnumerable Children { get { return ChildIds.Select(id => UmbracoContext.Current.ContentCache.GetById(id)); } } + + #endregion + + #region ContentSet + + public IEnumerable ContentSet { get { return this.Siblings(); } } + + #endregion + + #region ContentType + + public PublishedContentType ContentType { get; private set; } + + #endregion + + #region Properties + + public ICollection Properties { get; set; } + + public IPublishedProperty GetProperty(string alias) + { + return Properties.FirstOrDefault(p => p.PropertyTypeAlias.InvariantEquals(alias)); + } + + public IPublishedProperty GetProperty(string alias, bool recurse) + { + var property = GetProperty(alias); + if (recurse == false) return property; + + IPublishedContent content = this; + while (content != null && (property == null || property.HasValue == false)) + { + content = content.Parent; + property = content == null ? null : content.GetProperty(alias); + } + + return property; + } + + public object this[string alias] + { + get + { + var property = GetProperty(alias); + return property == null || property.HasValue == false ? null : property.ObjectValue; + } + } + + #endregion + } + + class SolidPublishedProperty : IPublishedProperty + { + public SolidPublishedProperty() + { + // initialize boring stuff + } + + public string PropertyTypeAlias { get; set; } + public object DataValue { get; set; } + public object ObjectValue { get; set; } + public bool HasValue { get; set; } + public object XPathValue { get; set; } + } + + [PublishedContentModel("ContentType2")] + public class ContentType2 : PublishedContentModel + { + #region Plumbing + + public ContentType2(IPublishedContent content) + : base(content) + { } + + #endregion + + // fast, if you know that the appropriate IPropertyEditorValueConverter is wired + public int Prop1 { get { return (int)this["prop1"]; } } + + // almost as fast, not sure I like it as much, though + //public int Prop1 { get { return this.GetPropertyValue("prop1"); } } + } + + [PublishedContentModel("ContentType2Sub")] + public class ContentType2Sub : ContentType2 + { + #region Plumbing + + public ContentType2Sub(IPublishedContent content) + : base(content) + { } + + #endregion + } + + class PublishedContentStrong1 : PublishedContentExtended + { + public PublishedContentStrong1(IPublishedContent content) + : base(content) + { } + + public int StrongValue { get { return (int)this["strongValue"]; } } + } + + class PublishedContentStrong1Sub : PublishedContentStrong1 + { + public PublishedContentStrong1Sub(IPublishedContent content) + : base(content) + { } + + public int AnotherValue { get { return (int)this["anotherValue"]; } } + } + + class PublishedContentStrong2 : PublishedContentExtended + { + public PublishedContentStrong2(IPublishedContent content) + : base(content) + { } + + public int StrongValue { get { return (int)this["strongValue"]; } } + } + + class AutoPublishedContentType : PublishedContentType + { + private static readonly PublishedPropertyType Default = new PublishedPropertyType("*", 0, Guid.Empty); + + public AutoPublishedContentType(int id, string alias, IEnumerable propertyTypes) + : base(id, alias, propertyTypes) + { } + + public override PublishedPropertyType GetPropertyType(string alias) + { + var propertyType = base.GetPropertyType(alias); + return propertyType ?? Default; + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 8582a894d5..ada63f7a80 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -1,8 +1,12 @@ +using System; +using System.Collections.Generic; using System.Linq; using System.Web; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; using Umbraco.Tests.TestHelpers; using Umbraco.Web; @@ -19,7 +23,58 @@ namespace Umbraco.Tests.PublishedContent get { return DatabaseBehavior.NoDatabasePerFixture; } } - protected override string GetXmlContent(int templateId) + private PluginManager _pluginManager; + + public override void Initialize() + { + // required so we can access property.Value + //PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver(); + + base.Initialize(); + + // this is so the model factory looks into the test assembly + _pluginManager = PluginManager.Current; + PluginManager.Current = new PluginManager(false) + { + AssembliesToScan = _pluginManager.AssembliesToScan + .Union(new[] { typeof(PublishedContentTests).Assembly }) + }; + + ApplicationContext.Current = new ApplicationContext(false) { IsReady = true }; + + // need to specify a custom callback for unit tests + // AutoPublishedContentTypes generates properties automatically + // when they are requested, but we must declare those that we + // explicitely want to be here... + + var propertyTypes = new[] + { + // AutoPublishedContentType will auto-generate other properties + new PublishedPropertyType("umbracoNaviHide", 0, Guid.Parse(Constants.PropertyEditors.TrueFalse)), + new PublishedPropertyType("selectedNodes", 0, Guid.Empty), + new PublishedPropertyType("umbracoUrlAlias", 0, Guid.Empty), + new PublishedPropertyType("content", 0, Guid.Parse(Constants.PropertyEditors.TinyMCEv3)), + new PublishedPropertyType("testRecursive", 0, Guid.Empty), + }; + var type = new AutoPublishedContentType(0, "anything", propertyTypes); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; + } + + public override void TearDown() + { + PluginManager.Current = _pluginManager; + ApplicationContext.Current.DisposeIfDisposable(); + ApplicationContext.Current = null; + } + + protected override void FreezeResolution() + { + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver( + new PublishedContentModelFactoryImpl()); + base.FreezeResolution(); + } + + protected override string GetXmlContent(int templateId) { return @" x.IsVisible())) + var items = doc + .Children + .Where(x => x.IsVisible()) + .ToContentSet(); + + Assert.AreEqual(3, items.Count()); + + foreach (var d in items) { - if (d.Id != 1178) + switch (d.Id) { - Assert.IsFalse(d.IsLast()); + case 1174: + Assert.IsTrue(d.IsFirst()); + Assert.IsFalse(d.IsLast()); + break; + case 1177: + Assert.IsFalse(d.IsFirst()); + Assert.IsFalse(d.IsLast()); + break; + case 1178: + Assert.IsFalse(d.IsFirst()); + Assert.IsTrue(d.IsLast()); + break; + default: + Assert.Fail("Invalid id."); + break; } - else + } + } + + [PublishedContentModel("Home")] + public class Home : PublishedContentModel + { + public Home(IPublishedContent content) + : base(content) + {} + } + + [Test] + public void Is_Last_From_Where_Filter2() + { + var doc = GetNode(1173); + + var items = doc.Children + .Select(PublishedContentModelFactory.CreateModel) // linq, returns IEnumerable + + // only way around this is to make sure every IEnumerable extension + // explicitely returns a PublishedContentSet, not an IEnumerable + + .OfType() // ours, return IEnumerable (actually a PublishedContentSet) + .Where(x => x.IsVisible()) // so, here it's linq again :-( + .ToContentSet() // so, we need that one for the test to pass + .ToArray(); + + Assert.AreEqual(1, items.Count()); + + foreach (var d in items) + { + switch (d.Id) { - Assert.IsTrue(d.IsLast()); + case 1174: + Assert.IsTrue(d.IsFirst()); + Assert.IsTrue(d.IsLast()); + break; + default: + Assert.Fail("Invalid id."); + break; } } } @@ -143,15 +258,17 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); - foreach (var d in doc.Children.Take(3)) + var items = doc.Children.Take(3).ToContentSet(); + + foreach (var item in items) { - if (d.Id != 1178) + if (item.Id != 1178) { - Assert.IsFalse(d.IsLast()); + Assert.IsFalse(item.IsLast()); } else { - Assert.IsTrue(d.IsLast()); + Assert.IsTrue(item.IsLast()); } } } @@ -179,16 +296,19 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetNode(1173); + var items = doc.Children + .Concat(new[] { GetNode(1175), GetNode(4444) }) + .ToContentSet(); - foreach (var d in doc.Children.Concat(new[] { GetNode(1175), GetNode(4444) })) + foreach (var item in items) { - if (d.Id != 4444) + if (item.Id != 4444) { - Assert.IsFalse(d.IsLast()); + Assert.IsFalse(item.IsLast()); } else { - Assert.IsTrue(d.IsLast()); + Assert.IsTrue(item.IsLast()); } } } @@ -234,15 +354,15 @@ namespace Umbraco.Tests.PublishedContent var doc = GetNode(1173); var propVal = doc.GetPropertyValue("content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal.GetType())); + Assert.IsInstanceOf(typeof(IHtmlString), propVal); Assert.AreEqual("
This is some content
", propVal.ToString()); var propVal2 = doc.GetPropertyValue("content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal2.GetType())); - Assert.AreEqual("
This is some content
", propVal2.ToString()); + Assert.IsInstanceOf(typeof(IHtmlString), propVal2); + Assert.AreEqual("
This is some content
", propVal2.ToString()); var propVal3 = doc.GetPropertyValue("Content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal3.GetType())); + Assert.IsInstanceOf(typeof(IHtmlString), propVal3); Assert.AreEqual("
This is some content
", propVal3.ToString()); } @@ -361,7 +481,6 @@ namespace Umbraco.Tests.PublishedContent } - [Test] public void HasValue() { @@ -374,14 +493,12 @@ namespace Umbraco.Tests.PublishedContent Assert.IsFalse(noValue); } - [Test] public void Ancestors_Where_Visible() { var doc = GetNode(1174); var whereVisible = doc.Ancestors().Where("Visible"); - Assert.AreEqual(1, whereVisible.Count()); } @@ -396,7 +513,6 @@ namespace Umbraco.Tests.PublishedContent Assert.IsTrue(visible.IsVisible()); } - [Test] public void Ancestor_Or_Self() { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index 8979af2215..ee7ba32a6d 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -90,15 +90,15 @@ namespace Umbraco.Tests.PublishedContent var publishedMedia = GetNode(media.Id); var propVal = publishedMedia.GetPropertyValue("content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal.GetType())); + Assert.IsInstanceOf(propVal); Assert.AreEqual("
This is some content
", propVal.ToString()); var propVal2 = publishedMedia.GetPropertyValue("content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal2.GetType())); + Assert.IsInstanceOf(propVal2); Assert.AreEqual("
This is some content
", propVal2.ToString()); var propVal3 = publishedMedia.GetPropertyValue("Content"); - Assert.IsTrue(TypeHelper.IsTypeAssignableFrom(propVal3.GetType())); + Assert.IsInstanceOf(propVal3); Assert.AreEqual("
This is some content
", propVal3.ToString()); } diff --git a/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs b/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs index d606be3c5b..f5df693982 100644 --- a/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs +++ b/src/Umbraco.Tests/PublishedContent/StronglyTypedModels/TypedModelBase.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web; namespace Umbraco.Tests.PublishedContent.StronglyTypedModels @@ -28,14 +29,11 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels /// by casting the typed model to IPublishedContent, so the properties doesn't show up by default: /// ie. ((IPublishedContent)textpage).Url /// - public abstract class TypedModelBase : IPublishedContent + public abstract class TypedModelBase : PublishedContentWrapped // IPublishedContent { - private readonly IPublishedContent _publishedContent; - protected TypedModelBase(IPublishedContent publishedContent) - { - _publishedContent = publishedContent; - } + : base(publishedContent) + { } protected readonly Func Property = MethodBase.GetCurrentMethod; protected readonly Func ContentTypeAlias = MethodBase.GetCurrentMethod; @@ -50,7 +48,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels protected T Resolve(string propertyTypeAlias) { - return _publishedContent.GetPropertyValue(propertyTypeAlias); + return Content.GetPropertyValue(propertyTypeAlias); } protected T Resolve(MethodBase methodBase, T ifCannotConvert) @@ -61,7 +59,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels protected T Resolve(string propertyTypeAlias, T ifCannotConvert) { - return _publishedContent.GetPropertyValue(propertyTypeAlias, false, ifCannotConvert); + return Content.GetPropertyValue(propertyTypeAlias, false, ifCannotConvert); } protected T Resolve(MethodBase methodBase, bool recursive, T ifCannotConvert) @@ -72,7 +70,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels protected T Resolve(string propertyTypeAlias, bool recursive, T ifCannotConvert) { - return _publishedContent.GetPropertyValue(propertyTypeAlias, recursive, ifCannotConvert); + return Content.GetPropertyValue(propertyTypeAlias, recursive, ifCannotConvert); } #endregion @@ -83,7 +81,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels if (constructorInfo == null) throw new Exception("No valid constructor found"); - return (T) constructorInfo.Invoke(new object[] {_publishedContent.Parent}); + return (T) constructorInfo.Invoke(new object[] {Content.Parent}); } protected IEnumerable Children(MethodBase methodBase) where T : TypedModelBase @@ -100,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels string singularizedDocTypeAlias = docTypeAlias.ToSingular(); - return _publishedContent.Children.Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) + return Content.Children.Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) .Select(x => (T)constructorInfo.Invoke(new object[] { x })); } @@ -118,7 +116,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels string singularizedDocTypeAlias = docTypeAlias.ToSingular(); - return _publishedContent.Ancestors().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) + return Content.Ancestors().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) .Select(x => (T)constructorInfo.Invoke(new object[] { x })); } @@ -136,42 +134,10 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels string singularizedDocTypeAlias = docTypeAlias.ToSingular(); - return _publishedContent.Descendants().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) + return Content.Descendants().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias) .Select(x => (T)constructorInfo.Invoke(new object[] { x })); } #endregion - - #region IPublishedContent - int IPublishedContent.Id { get { return _publishedContent.Id; } } - int IPublishedContent.TemplateId { get { return _publishedContent.TemplateId; } } - int IPublishedContent.SortOrder { get { return _publishedContent.SortOrder; } } - string IPublishedContent.Name { get { return _publishedContent.Name; } } - string IPublishedContent.UrlName { get { return _publishedContent.UrlName; } } - string IPublishedContent.DocumentTypeAlias { get { return _publishedContent.DocumentTypeAlias; } } - int IPublishedContent.DocumentTypeId { get { return _publishedContent.DocumentTypeId; } } - string IPublishedContent.WriterName { get { return _publishedContent.WriterName; } } - string IPublishedContent.CreatorName { get { return _publishedContent.CreatorName; } } - int IPublishedContent.WriterId { get { return _publishedContent.WriterId; } } - int IPublishedContent.CreatorId { get { return _publishedContent.CreatorId; } } - string IPublishedContent.Path { get { return _publishedContent.Path; } } - DateTime IPublishedContent.CreateDate { get { return _publishedContent.CreateDate; } } - DateTime IPublishedContent.UpdateDate { get { return _publishedContent.UpdateDate; } } - Guid IPublishedContent.Version { get { return _publishedContent.Version; } } - int IPublishedContent.Level { get { return _publishedContent.Level; } } - string IPublishedContent.Url { get { return _publishedContent.Url; } } - PublishedItemType IPublishedContent.ItemType { get { return _publishedContent.ItemType; } } - IPublishedContent IPublishedContent.Parent { get { return _publishedContent.Parent; } } - IEnumerable IPublishedContent.Children { get { return _publishedContent.Children; } } - - ICollection IPublishedContent.Properties { get { return _publishedContent.Properties; } } - - object IPublishedContent.this[string propertyAlias] { get { return _publishedContent[propertyAlias]; } } - - IPublishedContentProperty IPublishedContent.GetProperty(string alias) - { - return _publishedContent.GetProperty(alias); - } - #endregion } /// diff --git a/src/Umbraco.Tests/PublishedContent/StronglyTypedQueryTests.cs b/src/Umbraco.Tests/PublishedContent/StronglyTypedQueryTests.cs deleted file mode 100644 index cd974fa534..0000000000 --- a/src/Umbraco.Tests/PublishedContent/StronglyTypedQueryTests.cs +++ /dev/null @@ -1,434 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; -using Umbraco.Web.Models; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace Umbraco.Tests.PublishedContent -{ - [TestFixture] - public class StronglyTypedQueryTests : PublishedContentTestBase - { - public override void Initialize() - { - base.Initialize(); - } - - public override void TearDown() - { - base.TearDown(); - } - - protected override DatabaseBehavior DatabaseTestBehavior - { - get { return DatabaseBehavior.NoDatabasePerFixture; } - } - - protected override string GetXmlContent(int templateId) - { - return @" - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - -"; - } - - internal IPublishedContent GetNode(int id) - { - var ctx = UmbracoContext.Current; - var doc = ctx.ContentCache.GetById(id); - Assert.IsNotNull(doc); - return doc; - } - - - [Test] - public void Type_Test() - { - var doc = GetNode(1); - var result = doc.NewsArticles(TraversalType.Descendants).ToArray(); - Assert.AreEqual("John doe", result[0].ArticleAuthor); - Assert.AreEqual("John Smith", result[1].ArticleAuthor); - } - - - [Test] - public void As_Test() - { - var doc = GetNode(1); - var result = doc.AsHome(); - Assert.AreEqual("Test site", result.SiteName); - - Assert.Throws(() => doc.AsContentPage()); - } - - } - - //NOTE: Some of these class will be moved in to the core once all this is working the way we want - - #region Gen classes & supporting classes - - //TOOD: SD: This class could be the way that the UmbracoHelper deals with looking things up in the background, we might not - // even expose it publicly but it could handle any caching (per request) that might be required when looking up any objects... - // though we might not need it at all, not sure yet. - // However, what we need to do is implement the GetDocumentsByType method of the IPublishedStore, see the TODO there. - // It might be nicer to have a QueryContext on the UmbracoHelper (we can still keep the Content and TypedContent, etc... - // methods, but these would just wrap the QueryContext attached to it. Other methods on the QueryContext will be - // ContentByType, TypedContentByType, etc... then we can also have extension methods like below for strongly typed - // access like: GetAllHomes, GetAllNewsArticles, etc... - - //public class QueryDataContext - //{ - // private readonly IPublishedContentStore _contentStore; - // private readonly UmbracoContext _umbracoContext; - - // internal QueryDataContext(IPublishedContentStore contentStore, UmbracoContext umbracoContext) - // { - // _contentStore = contentStore; - // _umbracoContext = umbracoContext; - // } - - // public IPublishedContent GetDocumentById(int id) - // { - // return _contentStore.GetDocumentById(_umbracoContext, id); - // } - - // public IEnumerable GetByDocumentType(string alias) - // { - - // } - //} - - public enum TraversalType - { - Children, - Ancestors, - AncestorsOrSelf, - Descendants, - DescendantsOrSelf - } - - public static class StronglyTypedQueryExtensions - { - private static IEnumerable GetEnumerable(this IPublishedContent content, string docTypeAlias, TraversalType traversalType = TraversalType.Children) - { - switch (traversalType) - { - case TraversalType.Children: - return content.Children.Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.Ancestors: - return content.Ancestors().Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.AncestorsOrSelf: - return content.AncestorsOrSelf().Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.Descendants: - return content.Descendants().Where(x => x.DocumentTypeAlias == docTypeAlias); - case TraversalType.DescendantsOrSelf: - return content.DescendantsOrSelf().Where(x => x.DocumentTypeAlias == docTypeAlias); - default: - throw new ArgumentOutOfRangeException("traversalType"); - } - } - - private static T AsDocumentType(this IPublishedContent content, string alias, Func creator) - { - if (content.DocumentTypeAlias == alias) return creator(content); - throw new InvalidOperationException("The content type cannot be cast to " + typeof(T).FullName + " since it is type: " + content.DocumentTypeAlias); - } - - public static HomeContentItem AsHome(this IPublishedContent content) - { - return content.AsDocumentType("Home", x => new HomeContentItem(x)); - } - - public static IEnumerable Homes(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("Home", traversalType).Select(x => new HomeContentItem(x)); - } - - public static NewsArticleContentItem AsNewsArticle(this IPublishedContent content) - { - return content.AsDocumentType("NewsArticle", x => new NewsArticleContentItem(x)); - } - - public static IEnumerable NewsArticles(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("NewsArticle", traversalType).Select(x => new NewsArticleContentItem(x)); - } - - public static NewsLandingPageContentItem AsNewsLandingPage(this IPublishedContent content) - { - return content.AsDocumentType("NewsLandingPage", x => new NewsLandingPageContentItem(x)); - } - - public static IEnumerable NewsLandingPages(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("NewsLandingPage", traversalType).Select(x => new NewsLandingPageContentItem(x)); - } - - public static ContentPageContentItem AsContentPage(this IPublishedContent content) - { - return content.AsDocumentType("ContentPage", x => new ContentPageContentItem(x)); - } - - public static IEnumerable ContentPages(this IPublishedContent content, TraversalType traversalType = TraversalType.Children) - { - return content.GetEnumerable("ContentPage", traversalType).Select(x => new ContentPageContentItem(x)); - } - } - - [DebuggerDisplay("Content Id: {Id}, Name: {Name}")] - public class PublishedContentWrapper : IPublishedContent, IOwnerCollectionAware - { - protected IPublishedContent WrappedContent { get; private set; } - - public PublishedContentWrapper(IPublishedContent content) - { - WrappedContent = content; - } - - public string Url - { - get { return WrappedContent.Url; } - } - - public PublishedItemType ItemType - { - get { return WrappedContent.ItemType; } - } - - public IPublishedContent Parent - { - get { return WrappedContent.Parent; } - } - - public int Id - { - get { return WrappedContent.Id; } - } - public int TemplateId - { - get { return WrappedContent.TemplateId; } - } - public int SortOrder - { - get { return WrappedContent.SortOrder; } - } - public string Name - { - get { return WrappedContent.Name; } - } - public string UrlName - { - get { return WrappedContent.UrlName; } - } - public string DocumentTypeAlias - { - get { return WrappedContent.DocumentTypeAlias; } - } - public int DocumentTypeId - { - get { return WrappedContent.DocumentTypeId; } - } - public string WriterName - { - get { return WrappedContent.WriterName; } - } - public string CreatorName - { - get { return WrappedContent.CreatorName; } - } - public int WriterId - { - get { return WrappedContent.WriterId; } - } - public int CreatorId - { - get { return WrappedContent.CreatorId; } - } - public string Path - { - get { return WrappedContent.Path; } - } - public DateTime CreateDate - { - get { return WrappedContent.CreateDate; } - } - public DateTime UpdateDate - { - get { return WrappedContent.UpdateDate; } - } - public Guid Version - { - get { return WrappedContent.Version; } - } - public int Level - { - get { return WrappedContent.Level; } - } - public ICollection Properties - { - get { return WrappedContent.Properties; } - } - - public object this[string propertyAlias] - { - get { return GetProperty(propertyAlias).Value; } - } - - public IEnumerable Children - { - get { return WrappedContent.Children; } - } - public IPublishedContentProperty GetProperty(string alias) - { - return WrappedContent.GetProperty(alias); - } - - private IEnumerable _ownersCollection; - - /// - /// Need to get/set the owner collection when an item is returned from the result set of a query - /// - /// - /// Based on this issue here: http://issues.umbraco.org/issue/U4-1797 - /// - IEnumerable IOwnerCollectionAware.OwnersCollection - { - get - { - var publishedContentBase = WrappedContent as IOwnerCollectionAware; - if (publishedContentBase != null) - { - return publishedContentBase.OwnersCollection; - } - - //if the owners collection is null, we'll default to it's siblings - if (_ownersCollection == null) - { - //get the root docs if parent is null - _ownersCollection = this.Siblings(); - } - return _ownersCollection; - } - set - { - var publishedContentBase = WrappedContent as IOwnerCollectionAware; - if (publishedContentBase != null) - { - publishedContentBase.OwnersCollection = value; - } - else - { - _ownersCollection = value; - } - } - } - } - - public partial class HomeContentItem : ContentPageContentItem - { - public HomeContentItem(IPublishedContent content) - : base(content) - { - } - - public string SiteName - { - get { return WrappedContent.GetPropertyValue("siteName"); } - } - public string SiteDescription - { - get { return WrappedContent.GetPropertyValue("siteDescription"); } - } - } - - public partial class NewsLandingPageContentItem : ContentPageContentItem - { - public NewsLandingPageContentItem(IPublishedContent content) - : base(content) - { - } - - public string PageTitle - { - get { return WrappedContent.GetPropertyValue("pageTitle"); } - } - } - - public partial class NewsArticleContentItem : PublishedContentWrapper - { - public NewsArticleContentItem(IPublishedContent content) - : base(content) - { - } - - public string ArticleContent - { - get { return WrappedContent.GetPropertyValue("articleContent"); } - } - public DateTime ArticleDate - { - get { return WrappedContent.GetPropertyValue("articleDate"); } - } - public string ArticleAuthor - { - get { return WrappedContent.GetPropertyValue("articleAuthor"); } - } - } - - public partial class ContentPageContentItem : PublishedContentWrapper - { - public ContentPageContentItem(IPublishedContent content) - : base(content) - { - } - - public string BodyContent - { - get { return WrappedContent.GetPropertyValue("bodyContent"); } - } - } - - #endregion -} \ No newline at end of file diff --git a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs index f292dc9082..7aaba12df1 100644 --- a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs +++ b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs @@ -150,6 +150,8 @@ namespace Umbraco.Tests.Publishing var result1 = strategy.Publish(_homePage, 0); Assert.IsTrue(result1); Assert.IsTrue(_homePage.Published); + + //NOTE (MCH) This isn't persisted, so not really a good test as it will look like the result should be something else. foreach (var c in ServiceContext.ContentService.GetChildren(_homePage.Id)) { var r = strategy.Publish(c, 0); @@ -157,15 +159,20 @@ namespace Umbraco.Tests.Publishing Assert.IsTrue(c.Published); } - //ok, all are published except the deepest descendant, we will pass in a flag to include it to - //be published - var result = strategy.PublishWithChildrenInternal( - ServiceContext.ContentService.GetDescendants(_homePage).Concat(new[] { _homePage }), 0, true); - //there will be 4 here but only one "Success" the rest will be "SuccessAlreadyPublished" - Assert.AreEqual(1, result.Count(x => x.Result.StatusType == PublishStatusType.Success)); - Assert.AreEqual(3, result.Count(x => x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished)); - Assert.IsTrue(result.Single(x => x.Result.StatusType == PublishStatusType.Success).Success); - Assert.IsTrue(result.Single(x => x.Result.StatusType == PublishStatusType.Success).Result.ContentItem.Published); + //NOTE (MCH) when doing the test like this the Publish status will not actually have been persisted + //since its only updating a property. The actual persistence and publishing is done through the ContentService. + //So when descendants are fetched from the ContentService the Publish status will be "reset", which + //means the result will be 1 'SuccessAlreadyPublished' and 3 'Success' because the Homepage is + //inserted in the list and since that item has the status of already being Published it will be the one item + //with 'SuccessAlreadyPublished' + + var descendants = ServiceContext.ContentService.GetDescendants(_homePage).Concat(new[] {_homePage}); + var result = strategy.PublishWithChildrenInternal(descendants, 0, true); + + Assert.AreEqual(3, result.Count(x => x.Result.StatusType == PublishStatusType.Success)); + Assert.AreEqual(1, result.Count(x => x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished)); + Assert.IsTrue(result.First(x => x.Result.StatusType == PublishStatusType.Success).Success); + Assert.IsTrue(result.First(x => x.Result.StatusType == PublishStatusType.Success).Result.ContentItem.Published); } [NUnit.Framework.Ignore] diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs index 0636b3828f..a45ac7a12b 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -52,9 +52,9 @@ namespace Umbraco.Tests.Routing } - [TestCase("http://domain1.com/this/is/my/alias", "de-DE", -1001)] // alias to domain's page fails FIXME wanted? + [TestCase("http://domain1.com/this/is/my/alias", "de-DE", -1001)] // alias to domain's page fails - no alias on domain's home [TestCase("http://domain1.com/page2/alias", "de-DE", 10011)] // alias to sub-page works - [TestCase("http://domain1.com/en/flux", "en-US", -10011)] // alias to domain's page fails FIXME wanted? + [TestCase("http://domain1.com/en/flux", "en-US", -10011)] // alias to domain's page fails - no alias on domain's home [TestCase("http://domain1.com/endanger", "de-DE", 10011)] // alias to sub-page works, even with "en..." [TestCase("http://domain1.com/en/endanger", "en-US", -10011)] // no [TestCase("http://domain1.com/only/one/alias", "de-DE", 100111)] // ok diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 089fc25ae2..e15f05817d 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -842,12 +842,14 @@ namespace Umbraco.Tests.Services Assert.That(sut.GetValue("multilineText"), Is.EqualTo("Multiple lines \n in one box")); Assert.That(sut.GetValue("upload"), Is.EqualTo("/media/1234/koala.jpg")); Assert.That(sut.GetValue("label"), Is.EqualTo("Non-editable label")); - Assert.That(sut.GetValue("dateTime"), Is.EqualTo(content.GetValue("dateTime"))); + //SD: This is failing because the 'content' call to GetValue always has empty milliseconds + //MCH: I'm guessing this is an issue because of the format the date is actually stored as, right? Cause we don't do any formatting when saving or loading + Assert.That(sut.GetValue("dateTime").ToString("G"), Is.EqualTo(content.GetValue("dateTime").ToString("G"))); Assert.That(sut.GetValue("colorPicker"), Is.EqualTo("black")); Assert.That(sut.GetValue("folderBrowser"), Is.Empty); Assert.That(sut.GetValue("ddlMultiple"), Is.EqualTo("1234,1235")); Assert.That(sut.GetValue("rbList"), Is.EqualTo("random")); - Assert.That(sut.GetValue("date"), Is.EqualTo(content.GetValue("date"))); + Assert.That(sut.GetValue("date").ToString("G"), Is.EqualTo(content.GetValue("date").ToString("G"))); Assert.That(sut.GetValue("ddl"), Is.EqualTo("1234")); Assert.That(sut.GetValue("chklist"), Is.EqualTo("randomc")); Assert.That(sut.GetValue("contentPicker"), Is.EqualTo(1090)); diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 5ed1c7ec09..f733ee65c2 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -13,6 +13,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; @@ -188,6 +189,12 @@ namespace Umbraco.Tests.TestHelpers MappingResolver.Current = new MappingResolver( () => PluginManager.Current.ResolveAssignedMapperTypes()); + if (PropertyValueConvertersResolver.HasCurrent == false) + PropertyValueConvertersResolver.Current = new PropertyValueConvertersResolver(); + + if (PublishedContentModelFactoryResolver.HasCurrent == false) + PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(); + base.FreezeResolution(); } diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index 95e89468c3..064e48dd7f 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -7,11 +7,13 @@ using SQLCE4Umbraco; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; using Umbraco.Core.Services; +using Umbraco.Tests.PublishedContent; using Umbraco.Tests.Stubs; using Umbraco.Web; using Umbraco.Web.Routing; @@ -27,7 +29,12 @@ namespace Umbraco.Tests.TestHelpers [SetUp] public override void Initialize() { - base.Initialize(); + base.Initialize(); + + // need to specify a custom callback for unit tests + // AutoPublishedContentTypes generates properties automatically + var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] {}); + PublishedContentType.GetPublishedContentTypeCallback = (alias) => type; } [TearDown] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 38f3f87c8c..b72b6d004c 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -237,6 +237,8 @@ + + @@ -271,6 +273,7 @@ + @@ -315,9 +318,9 @@ + - @@ -359,6 +362,7 @@ + diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 74cb47e9a7..1358c28ad9 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -7,6 +7,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.Caching; using Umbraco.Web.PublishedCache; @@ -129,7 +130,8 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.ContentTypeCacheKey); //clear static object cache global::umbraco.cms.businesslogic.ContentType.RemoveAllDataTypeCache(); - PublishedContentHelper.ClearPropertyTypeCache(); + + PublishedContentType.ClearAll(); base.RefreshAll(); } @@ -251,7 +253,8 @@ namespace Umbraco.Web.Cache //clears the dictionary object cache of the legacy ContentType global::umbraco.cms.businesslogic.ContentType.RemoveFromDataTypeCache(payload.Alias); - PublishedContentHelper.ClearPropertyTypeCache(); + + PublishedContentType.ClearContentType(payload.Id); //need to recursively clear the cache for each child content type foreach (var descendant in payload.DescendantPayloads) diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index fb792fac27..7691c1a125 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using System.Linq; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Cache { @@ -121,6 +122,8 @@ namespace Umbraco.Web.Cache string.Format("{0}{1}", CacheKeys.DataTypeCacheKey, payload.Id)); ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch( string.Format("{0}{1}", CacheKeys.DataTypeCacheKey, payload.UniqueId)); + + PublishedContentType.ClearDataType(payload.Id); }); base.Refresh(jsonPayload); diff --git a/src/Umbraco.Web/ContextualPublishedCacheExtensions.cs b/src/Umbraco.Web/ContextualPublishedCacheExtensions.cs index d8e976cf8c..2901962f07 100644 --- a/src/Umbraco.Web/ContextualPublishedCacheExtensions.cs +++ b/src/Umbraco.Web/ContextualPublishedCacheExtensions.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web public static dynamic GetDynamicById(this ContextualPublishedContentCache cache, int contentId) { var content = cache.GetById(contentId); - return content == null ? new DynamicNull() : new DynamicPublishedContent(content).AsDynamic(); + return content == null ? DynamicNull.Null : new DynamicPublishedContent(content).AsDynamic(); } /// @@ -34,7 +34,7 @@ namespace Umbraco.Web public static dynamic GetDynamicSingleByXPath(this ContextualPublishedContentCache cache, string xpath, params XPathVariable[] vars) { var content = cache.GetSingleByXPath(xpath, vars); - return content == null ? new DynamicNull() : new DynamicPublishedContent(content).AsDynamic(); + return content == null ? DynamicNull.Null : new DynamicPublishedContent(content).AsDynamic(); } /// @@ -47,7 +47,7 @@ namespace Umbraco.Web public static dynamic GetDynamicSingleByXPath(this ContextualPublishedContentCache cache, XPathExpression xpath, params XPathVariable[] vars) { var content = cache.GetSingleByXPath(xpath, vars); - return content == null ? new DynamicNull() : new DynamicPublishedContent(content).AsDynamic(); + return content == null ? DynamicNull.Null : new DynamicPublishedContent(content).AsDynamic(); } /// diff --git a/src/Umbraco.Web/Dynamics/ExtensionMethods.cs b/src/Umbraco.Web/Dynamics/ExtensionMethods.cs index 316b4951bf..dd3d2f69cd 100644 --- a/src/Umbraco.Web/Dynamics/ExtensionMethods.cs +++ b/src/Umbraco.Web/Dynamics/ExtensionMethods.cs @@ -1,32 +1,31 @@ using System; +using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Models; using Umbraco.Web.Models; namespace Umbraco.Web.Dynamics { internal static class ExtensionMethods { - - - public static DynamicPublishedContentList Random(this DynamicPublishedContentList all, int min, int max) + public static DynamicPublishedContentList Random(this DynamicPublishedContentList source, int min, int max) { - //get a random number generator - Random r = new Random(); - //choose the number of elements to be returned between Min and Max - int Number = r.Next(min, max); - //Call the other method - return Random(all, Number); - } - public static DynamicPublishedContentList Random(this DynamicPublishedContentList all, int max) - { - //Randomly order the items in the set by a Guid, Take the correct number, and return this wrapped in a new DynamicNodeList - return new DynamicPublishedContentList(all.Items.OrderBy(x => Guid.NewGuid()).Take(max)); + return Random(source, new Random().Next(min, max)); } - public static DynamicPublishedContent Random(this DynamicPublishedContentList all) + public static DynamicPublishedContentList Random(this DynamicPublishedContentList source, int max) { - return all.Items.OrderBy(x => Guid.NewGuid()).First(); + return new DynamicPublishedContentList(source.OrderByRandom().Take(max)); } + public static DynamicPublishedContent Random(this DynamicPublishedContentList source) + { + return new DynamicPublishedContent(source.OrderByRandom().First()); + } + + private static IEnumerable OrderByRandom(this DynamicPublishedContentList source) + { + return source.Items.OrderBy(x => Guid.NewGuid()); + } } } diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index cd309fe180..89f6fbe7cf 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml; using Examine; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; -using Umbraco.Web.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache; namespace Umbraco.Web @@ -15,25 +14,39 @@ namespace Umbraco.Web /// internal static class ExamineExtensions { - internal static IEnumerable ConvertSearchResultToPublishedContent( - this IEnumerable results, + internal static PublishedContentSet ConvertSearchResultToPublishedContent(this IEnumerable results, ContextualPublishedCache cache) { //TODO: The search result has already returned a result which SHOULD include all of the data to create an IPublishedContent, - // however thsi is currently not the case: + // however this is currently not the case: // http://examine.codeplex.com/workitem/10350 - var list = new List(); + var list = new List(); + var set = new PublishedContentSet(list); foreach (var result in results.OrderByDescending(x => x.Score)) { - var doc = cache.GetById(result.Id); - if (doc == null) continue; //skip if this doesn't exist in the cache - doc.Properties.Add( - new PropertyResult("examineScore", result.Score.ToString(), PropertyResultType.CustomProperty)); - list.Add(doc); + var content = cache.GetById(result.Id); + if (content == null) continue; // skip if this doesn't exist in the cache + + // need to extend the content as we're going to add a property to it, + // and we should not ever do it to the content we get from the cache, + // precisely because it is cached and shared by all requests. + + // but we cannot wrap it because we need to respect the type that was + // returned by the cache, in case the cache can create real types. + // so we have to ask it to please extend itself. + + list.Add(content); + var extend = set.MapContent(content); + + var property = new PropertyResult("examineScore", + result.Score, + PropertyResultType.CustomProperty); + extend.AddProperty(property); } - return list; + + return set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Macros/PartialViewMacroController.cs b/src/Umbraco.Web/Macros/PartialViewMacroController.cs index b858d1c283..e275464b20 100644 --- a/src/Umbraco.Web/Macros/PartialViewMacroController.cs +++ b/src/Umbraco.Web/Macros/PartialViewMacroController.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Macros public PartialViewResult Index() { var model = new PartialViewMacroModel( - _currentPage.ConvertFromNode(), + _umbracoContext.ContentCache.GetById(_currentPage.Id), //_currentPage.ConvertFromNode(), _macro.Id, _macro.Alias, _macro.Name, diff --git a/src/Umbraco.Web/Models/DynamicPublishedContent.cs b/src/Umbraco.Web/Models/DynamicPublishedContent.cs index 7771517419..21d2fad7bc 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContent.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContent.cs @@ -1,18 +1,19 @@ -using System; +// fixme - should #define - but when will it be OK? +#undef FIX_GET_PROPERTY_VALUE + +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Dynamic; using System.Linq; +using System.Runtime.CompilerServices; using System.Web; -using Umbraco.Core.Configuration; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using Umbraco.Core; -using Umbraco.Core.PropertyEditors; using System.Reflection; -using System.Xml.Linq; -using umbraco.cms.businesslogic; +using Umbraco.Core.Models.PublishedContent; using ContentType = umbraco.cms.businesslogic.ContentType; namespace Umbraco.Web.Models @@ -22,73 +23,46 @@ namespace Umbraco.Web.Models /// The base dynamic model for views /// [DebuggerDisplay("Content Id: {Id}, Name: {Name}")] - public class DynamicPublishedContent : DynamicObject, IPublishedContent, IOwnerCollectionAware + public class DynamicPublishedContent : DynamicObject, IPublishedContent { protected internal IPublishedContent PublishedContent { get; private set; } - private DynamicPublishedContentList _cachedChildren; - private readonly ConcurrentDictionary _cachedMemberOutput = new ConcurrentDictionary(); - + private DynamicPublishedContentList _contentList; + + // must implement that one if we implement IPublishedContent + public IEnumerable ContentSet + { + // that is a definitively non-efficient way of doing it, though it should work + get { return _contentList ?? (_contentList = new DynamicPublishedContentList(PublishedContent.ContentSet)); } + } + + public PublishedContentType ContentType { get { return PublishedContent.ContentType; } } + #region Constructors public DynamicPublishedContent(IPublishedContent content) { if (content == null) throw new ArgumentNullException("content"); PublishedContent = content; - } + } + + internal DynamicPublishedContent(IPublishedContent content, DynamicPublishedContentList contentList) + { + PublishedContent = content; + _contentList = contentList; + } #endregion - private IEnumerable _ownersCollection; + // these two here have leaked in v6 and so we cannot remove them anymore + // without breaking compatibility but... TODO: remove them in v7 + public DynamicPublishedContentList ChildrenAsList { get { return Children; } } + public int parentId { get { return PublishedContent.Parent.Id; } } + + #region DynamicObject + + private readonly ConcurrentDictionary _cachedMemberOutput = new ConcurrentDictionary(); /// - /// Need to get/set the owner collection when an item is returned from the result set of a query - /// - /// - /// Based on this issue here: http://issues.umbraco.org/issue/U4-1797 - /// - IEnumerable IOwnerCollectionAware.OwnersCollection - { - get - { - var publishedContentBase = PublishedContent as IOwnerCollectionAware; - if (publishedContentBase != null) - { - return publishedContentBase.OwnersCollection; - } - - //if the owners collection is null, we'll default to it's siblings - if (_ownersCollection == null) - { - //get the root docs if parent is null - _ownersCollection = this.Siblings(); - } - return _ownersCollection; - } - set - { - var publishedContentBase = PublishedContent as IOwnerCollectionAware; - if (publishedContentBase != null) - { - publishedContentBase.OwnersCollection = value; - } - else - { - _ownersCollection = value; - } - } - } - - public dynamic AsDynamic() - { - return this; - } - - public bool HasProperty(string name) - { - return PublishedContent.HasProperty(name); - } - - /// /// Attempts to call a method on the dynamic object /// /// @@ -130,10 +104,11 @@ namespace Umbraco.Web.Models } //this is the result of an extension method execution gone wrong so we return dynamic null - if (attempt.Result.Reason == DynamicInstanceHelper.TryInvokeMemberSuccessReason.FoundExtensionMethod - && attempt.Exception != null && attempt.Exception is TargetInvocationException) + if (attempt.Result != null + && attempt.Result.Reason == DynamicInstanceHelper.TryInvokeMemberSuccessReason.FoundExtensionMethod + && attempt.Exception is TargetInvocationException) { - result = new DynamicNull(); + result = DynamicNull.Null; return true; } @@ -148,6 +123,10 @@ namespace Umbraco.Web.Models /// protected virtual Attempt TryGetCustomMember(GetMemberBinder binder) { + // as of 4.5 the CLR is case-sensitive which means that the default binder + // will handle those methods only when using the proper casing. So what + // this method does is ensure that any casing is supported. + if (binder.Name.InvariantEquals("ChildrenAsList") || binder.Name.InvariantEquals("Children")) { return Attempt.Succeed(Children); @@ -162,6 +141,7 @@ namespace Umbraco.Web.Models } return Attempt.Succeed(parent.Id); } + return Attempt.Fail(); } @@ -195,10 +175,9 @@ namespace Umbraco.Web.Models /// protected virtual Attempt TryGetDocumentProperty(GetMemberBinder binder) { - var reflectedProperty = GetReflectedProperty(binder.Name); var result = reflectedProperty != null - ? reflectedProperty.Value + ? reflectedProperty.ObjectValue : null; return Attempt.If(result != null, result); @@ -212,44 +191,15 @@ namespace Umbraco.Web.Models protected virtual Attempt TryGetUserProperty(GetMemberBinder binder) { var name = binder.Name; - var recursive = false; + var recurse = false; if (name.StartsWith("_")) { name = name.Substring(1, name.Length - 1); - recursive = true; + recurse = true; } - var userProperty = GetUserProperty(name, recursive); - - if (userProperty == null) - { - return Attempt.Fail(); - } - - var result = userProperty.Value; - - if (PublishedContent.DocumentTypeAlias == null && userProperty.Alias == null) - { - throw new InvalidOperationException("No node alias or property alias available. Unable to look up the datatype of the property you are trying to fetch."); - } - - //get the property editor alias for the current property - var propertyEditor = PublishedContentHelper.GetPropertyEditor( - ApplicationContext.Current, - userProperty.DocumentTypeAlias, - userProperty.Alias, - ItemType); - - //convert the string value to a known type - var def = new PublishedPropertyDefinition(userProperty.Alias, userProperty.DocumentTypeAlias, propertyEditor); - var converted = PublishedContentHelper.ConvertPropertyValue(result, def); - if (converted.Success) - { - result = converted.Result; - } - - return Attempt.Succeed(result); - + var value = PublishedContent.GetPropertyValue(name, recurse); + return Attempt.SucceedIf(value != null, value); } /// @@ -312,7 +262,7 @@ namespace Umbraco.Web.Models //and will make it false //which means backwards equality (&& property != true) will pass //forwwards equality (&& property or && property == true) will fail - result = new DynamicNull(); + result = DynamicNull.Null; //alwasy return true if we haven't thrown an exception though I'm wondering if we return 'false' if .Net throws an exception for us?? return true; @@ -343,20 +293,19 @@ namespace Umbraco.Web.Models var context = this; var prop = GetPropertyInternal(alias, PublishedContent); - while (prop == null || !prop.HasValue()) + while (prop == null || !prop.HasValue) { var parent = ((IPublishedContent) context).Parent; if (parent == null) break; // Update the context before attempting to retrieve the property again. - context = parent.AsDynamicPublishedContent(); + context = parent.AsDynamicOrNull(); prop = context.GetPropertyInternal(alias, context.PublishedContent); } return prop; } - private PropertyResult GetPropertyInternal(string alias, IPublishedContent content, bool checkUserProperty = true) { if (alias.IsNullOrWhiteSpace()) throw new ArgumentNullException("alias"); @@ -367,17 +316,18 @@ namespace Umbraco.Web.Models { var prop = content.GetProperty(alias); - return prop == null - ? null - : new PropertyResult(prop, PropertyResultType.UserProperty) - { - DocumentTypeAlias = content.DocumentTypeAlias, - DocumentId = content.Id - }; + // get wrap the result in a PropertyResult - just so it's an IHtmlString - ?! + return prop == null + ? null + : new PropertyResult(prop, PropertyResultType.UserProperty); } //reflect + // as of 4.5 the CLR is case-sensitive which means that the default binder + // can handle properties only when using the proper casing. So what this + // does is ensure that any casing is supported. + Func> getMember = memberAlias => { @@ -409,91 +359,128 @@ namespace Umbraco.Web.Models } return !attempt.Success - ? null - : new PropertyResult(alias, attempt.Result, PropertyResultType.ReflectedProperty) - { - DocumentTypeAlias = content.DocumentTypeAlias, - DocumentId = content.Id - }; + ? null + : new PropertyResult(alias, attempt.Result, PropertyResultType.ReflectedProperty); } - + #endregion - //public DynamicNode Media(string propertyAlias) - //{ - // if (_n != null) - // { - // IProperty prop = _n.GetProperty(propertyAlias); - // if (prop != null) - // { - // int mediaNodeId; - // if (int.TryParse(prop.Value, out mediaNodeId)) - // { - // return _razorLibrary.Value.MediaById(mediaNodeId); - // } - // } - // return null; - // } - // return null; - //} - //public bool IsProtected - //{ - // get - // { - // if (_n != null) - // { - // return umbraco.library.IsProtected(_n.Id, _n.Path); - // } - // return false; - // } - //} - //public bool HasAccess - //{ - // get - // { - // if (_n != null) - // { - // return umbraco.library.HasAccess(_n.Id, _n.Path); - // } - // return true; - // } - //} + #region Explicit IPublishedContent implementation - //public string Media(string propertyAlias, string mediaPropertyAlias) - //{ - // if (_n != null) - // { - // IProperty prop = _n.GetProperty(propertyAlias); - // if (prop == null && propertyAlias.Substring(0, 1).ToUpper() == propertyAlias.Substring(0, 1)) - // { - // prop = _n.GetProperty(propertyAlias.Substring(0, 1).ToLower() + propertyAlias.Substring((1))); - // } - // if (prop != null) - // { - // int mediaNodeId; - // if (int.TryParse(prop.Value, out mediaNodeId)) - // { - // umbraco.cms.businesslogic.media.Media media = new cms.businesslogic.media.Media(mediaNodeId); - // if (media != null) - // { - // Property mprop = media.getProperty(mediaPropertyAlias); - // // check for nicer support of Pascal Casing EVEN if alias is camelCasing: - // if (prop == null && mediaPropertyAlias.Substring(0, 1).ToUpper() == mediaPropertyAlias.Substring(0, 1)) - // { - // mprop = media.getProperty(mediaPropertyAlias.Substring(0, 1).ToLower() + mediaPropertyAlias.Substring((1))); - // } - // if (mprop != null) - // { - // return string.Format("{0}", mprop.Value); - // } - // } - // } - // } - // } - // return null; - //} + IPublishedContent IPublishedContent.Parent + { + get { return PublishedContent.Parent; } + } + + int IPublishedContent.Id + { + get { return PublishedContent.Id; } + } + + int IPublishedContent.TemplateId + { + get { return PublishedContent.TemplateId; } + } + + int IPublishedContent.SortOrder + { + get { return PublishedContent.SortOrder; } + } + + string IPublishedContent.Name + { + get { return PublishedContent.Name; } + } + + string IPublishedContent.UrlName + { + get { return PublishedContent.UrlName; } + } + + string IPublishedContent.DocumentTypeAlias + { + get { return PublishedContent.DocumentTypeAlias; } + } + + int IPublishedContent.DocumentTypeId + { + get { return PublishedContent.DocumentTypeId; } + } + + string IPublishedContent.WriterName + { + get { return PublishedContent.WriterName; } + } + + string IPublishedContent.CreatorName + { + get { return PublishedContent.CreatorName; } + } + + int IPublishedContent.WriterId + { + get { return PublishedContent.WriterId; } + } + + int IPublishedContent.CreatorId + { + get { return PublishedContent.CreatorId; } + } + + string IPublishedContent.Path + { + get { return PublishedContent.Path; } + } + + DateTime IPublishedContent.CreateDate + { + get { return PublishedContent.CreateDate; } + } + + DateTime IPublishedContent.UpdateDate + { + get { return PublishedContent.UpdateDate; } + } + + Guid IPublishedContent.Version + { + get { return PublishedContent.Version; } + } + + int IPublishedContent.Level + { + get { return PublishedContent.Level; } + } + + bool IPublishedContent.IsDraft + { + get { return PublishedContent.IsDraft; } + } + + int IPublishedContent.GetIndex() + { + return PublishedContent.GetIndex(); + } + + ICollection IPublishedContent.Properties + { + get { return PublishedContent.Properties; } + } + + IEnumerable IPublishedContent.Children + { + get { return PublishedContent.Children; } + } + + IPublishedProperty IPublishedContent.GetProperty(string alias) + { + return PublishedContent.GetProperty(alias); + } + + #endregion + + #region IPublishedContent implementation - #region Standard Properties public int TemplateId { get { return PublishedContent.TemplateId; } @@ -509,16 +496,6 @@ namespace Umbraco.Web.Models get { return PublishedContent.Name; } } - public bool Visible - { - get { return PublishedContent.IsVisible(); } - } - - public bool IsVisible() - { - return PublishedContent.IsVisible(); - } - public string UrlName { get { return PublishedContent.UrlName; } @@ -589,7 +566,13 @@ namespace Umbraco.Web.Models get { return PublishedContent.ItemType; } } - public IEnumerable Properties + // see note in IPublishedContent + //public bool Published + //{ + // get { return PublishedContent.Published; } + //} + + public IEnumerable Properties { get { return PublishedContent.Properties; } } @@ -599,629 +582,712 @@ namespace Umbraco.Web.Models get { return PublishedContent[propertyAlias]; } } - public DynamicPublishedContentList Children - { - get - { - if (_cachedChildren == null) - { - var children = PublishedContent.Children; - //testing, think this must be a special case for the root node ? - if (!children.Any() && PublishedContent.Id == 0) - { - _cachedChildren = new DynamicPublishedContentList(new List { new DynamicPublishedContent(this.PublishedContent) }); - } - else - { - _cachedChildren = new DynamicPublishedContentList(PublishedContent.Children.Select(x => new DynamicPublishedContent(x))); - } - } - return _cachedChildren; - } - } - #endregion + #endregion - public string GetTemplateAlias() + #region GetProperty + + // enhanced versions of the extension methods that exist for IPublishedContent, + // here we support the recursive (_) and reflected (@) syntax + + public IPublishedProperty GetProperty(string alias) + { + return alias.StartsWith("_") + ? GetProperty(alias.Substring(1), true) + : GetProperty(alias, false); + } + + public IPublishedProperty GetProperty(string alias, bool recurse) + { + if (alias.StartsWith("@")) return GetReflectedProperty(alias.Substring(1)); + + // get wrap the result in a PropertyResult - just so it's an IHtmlString - ?! + var property = PublishedContent.GetProperty(alias, recurse); + return property == null ? null : new PropertyResult(property, PropertyResultType.UserProperty); + } + + #endregion + + // IPublishedContent extension methods: + // + // all these methods are IPublishedContent extension methods so they should in + // theory apply to DynamicPublishedContent since it is an IPublishedContent and + // we look for extension methods. But that lookup has to be pretty slow. + // Duplicating the methods here makes things much faster. + + #region IPublishedContent extension methods - Template + + public string GetTemplateAlias() { return PublishedContentExtensions.GetTemplateAlias(this); } + + #endregion - #region Search + #region IPublishedContent extension methods - HasProperty - public DynamicPublishedContentList Search(string term, bool useWildCards = true, string searchProvider = null) + public bool HasProperty(string name) + { + return PublishedContent.HasProperty(name); + } + + #endregion + + #region IPublishedContent extension methods - HasValue + + public bool HasValue(string alias) { - return new DynamicPublishedContentList( - PublishedContentExtensions.Search(this, term, useWildCards, searchProvider)); + return PublishedContent.HasValue(alias); } - public DynamicPublishedContentList SearchDescendants(string term, bool useWildCards = true, string searchProvider = null) - { - return new DynamicPublishedContentList( - PublishedContentExtensions.SearchDescendants(this, term, useWildCards, searchProvider)); - } - - public DynamicPublishedContentList SearchChildren(string term, bool useWildCards = true, string searchProvider = null) - { - return new DynamicPublishedContentList( - PublishedContentExtensions.SearchChildren(this, term, useWildCards, searchProvider)); - } - - public DynamicPublishedContentList Search(Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) - { - return new DynamicPublishedContentList( - PublishedContentExtensions.Search(this, criteria, searchProvider)); - } - - #endregion - - #region GetProperty methods which can be used with the dynamic object - - public IPublishedContentProperty GetProperty(string alias) - { - var prop = GetProperty(alias, false); - if (prop == null && alias.StartsWith("_")) - { - //if it is prefixed and the first result failed, try to get it by recursive - var recursiveAlias = alias.Substring(1, alias.Length - 1); - return GetProperty(recursiveAlias, true); - } - return prop; - } - public IPublishedContentProperty GetProperty(string alias, bool recursive) - { - return alias.StartsWith("@") - ? GetReflectedProperty(alias.TrimStart('@')) - : GetUserProperty(alias, recursive); - } - public string GetPropertyValue(string alias) - { - return GetPropertyValue(alias, false); - } - public string GetPropertyValue(string alias, string fallback) - { - var prop = GetPropertyValue(alias); - return !prop.IsNullOrWhiteSpace() ? prop : fallback; - } - public string GetPropertyValue(string alias, bool recursive) - { - var p = alias.StartsWith("@") - ? GetReflectedProperty(alias.TrimStart('@')) - : GetUserProperty(alias, recursive); - return p == null ? null : p.ValueAsString; - } - public string GetPropertyValue(string alias, bool recursive, string fallback) - { - var prop = GetPropertyValue(alias, recursive); - return !prop.IsNullOrWhiteSpace() ? prop : fallback; - } - - #endregion - - #region HasValue - public bool HasValue(string alias) - { - return this.PublishedContent.HasValue(alias); - } public bool HasValue(string alias, bool recursive) { - return this.PublishedContent.HasValue(alias, recursive); + return PublishedContent.HasValue(alias, recursive); } + public IHtmlString HasValue(string alias, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.HasValue(alias, valueIfTrue, valueIfFalse); + return PublishedContent.HasValue(alias, valueIfTrue, valueIfFalse); } + public IHtmlString HasValue(string alias, bool recursive, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.HasValue(alias, recursive, valueIfTrue, valueIfFalse); + return PublishedContent.HasValue(alias, recursive, valueIfTrue, valueIfFalse); } + public IHtmlString HasValue(string alias, string valueIfTrue) { - return this.PublishedContent.HasValue(alias, valueIfTrue); + return PublishedContent.HasValue(alias, valueIfTrue); } + public IHtmlString HasValue(string alias, bool recursive, string valueIfTrue) { - return this.PublishedContent.HasValue(alias, recursive, valueIfTrue); + return PublishedContent.HasValue(alias, recursive, valueIfTrue); } + #endregion - #region Explicit IPublishedContent implementation + #region IPublishedContent extension methods - GetPropertyValue - IPublishedContent IPublishedContent.Parent - { - get { return PublishedContent.Parent; } - } + // for whatever reason, some methods returning strings were created in DynamicPublishedContent + // and are now considered a "feature" as of v6. So we can't have the proper GetPropertyValue + // methods returning objects, too. And we don't want to change it in v6 as that would be a + // breaking change. - int IPublishedContent.Id - { - get { return PublishedContent.Id; } - } +#if FIX_GET_PROPERTY_VALUE - int IPublishedContent.TemplateId - { - get { return PublishedContent.TemplateId; } - } + public object GetPropertyValue(string alias) + { + return PublishedContent.GetPropertyValue(alias); + } - int IPublishedContent.SortOrder - { - get { return PublishedContent.SortOrder; } - } + public object GetPropertyValue(string alias, string defaultValue) + { + return PublishedContent.GetPropertyValue(alias, defaultValue); + } - string IPublishedContent.Name - { - get { return PublishedContent.Name; } - } + public object GetPropertyValue(string alias, object defaultValue) + { + return PublishedContent.GetPropertyValue(alias, defaultValue); + } - string IPublishedContent.UrlName - { - get { return PublishedContent.UrlName; } - } + public object GetPropertyValue(string alias, bool recurse) + { + return PublishedContent.GetPropertyValue(alias, recurse); + } - string IPublishedContent.DocumentTypeAlias - { - get { return PublishedContent.DocumentTypeAlias; } - } + public object GetPropertyValue(string alias, bool recurse, object defaultValue) + { + return PublishedContent.GetPropertyValue(alias, recurse, defaultValue); + } - int IPublishedContent.DocumentTypeId - { - get { return PublishedContent.DocumentTypeId; } - } +#else - string IPublishedContent.WriterName - { - get { return PublishedContent.WriterName; } - } + public string GetPropertyValue(string alias) + { + return GetPropertyValue(alias, false); + } - string IPublishedContent.CreatorName - { - get { return PublishedContent.CreatorName; } - } + public string GetPropertyValue(string alias, string defaultValue) + { + var value = GetPropertyValue(alias); + return value.IsNullOrWhiteSpace() ? defaultValue : value; + } - int IPublishedContent.WriterId - { - get { return PublishedContent.WriterId; } - } + public string GetPropertyValue(string alias, bool recurse, string defaultValue) + { + var value = GetPropertyValue(alias, recurse); + return value.IsNullOrWhiteSpace() ? defaultValue : value; + } - int IPublishedContent.CreatorId - { - get { return PublishedContent.CreatorId; } - } + public string GetPropertyValue(string alias, bool recursive) + { + var property = GetProperty(alias, recursive); + if (property == null || property.ObjectValue == null) return null; + return property.ObjectValue.ToString(); + } - string IPublishedContent.Path - { - get { return PublishedContent.Path; } - } +#endif - DateTime IPublishedContent.CreateDate - { - get { return PublishedContent.CreateDate; } - } + #endregion - DateTime IPublishedContent.UpdateDate - { - get { return PublishedContent.UpdateDate; } - } + #region IPublishedContent extension methods - GetPropertyValue - Guid IPublishedContent.Version - { - get { return PublishedContent.Version; } - } + public T GetPropertyValue(string alias) + { + return PublishedContent.GetPropertyValue(alias); + } - int IPublishedContent.Level - { - get { return PublishedContent.Level; } - } + public T GetPropertyValue(string alias, T defaultValue) + { + return PublishedContent.GetPropertyValue(alias, defaultValue); + } - ICollection IPublishedContent.Properties - { - get { return PublishedContent.Properties; } - } + public T GetPropertyValue(string alias, bool recurse) + { + return PublishedContent.GetPropertyValue(alias, recurse); + } - IEnumerable IPublishedContent.Children - { - get { return PublishedContent.Children; } - } + public T GetPropertyValue(string alias, bool recurse, T defaultValue) + { + return PublishedContent.GetPropertyValue(alias, recurse, defaultValue); + } - IPublishedContentProperty IPublishedContent.GetProperty(string alias) - { - return PublishedContent.GetProperty(alias); - } - #endregion + #endregion - - #region Index/Position - public int Position() - { - return Umbraco.Web.PublishedContentExtensions.Position(this); - } - public int Index() - { - return Umbraco.Web.PublishedContentExtensions.Index(this); - } - #endregion + #region IPublishedContent extension methods - Search - #region Is Helpers + public DynamicPublishedContentList Search(string term, bool useWildCards = true, string searchProvider = null) + { + return new DynamicPublishedContentList(PublishedContent.Search(term, useWildCards, searchProvider)); + } + + public DynamicPublishedContentList SearchDescendants(string term, bool useWildCards = true, string searchProvider = null) + { + return new DynamicPublishedContentList(PublishedContent.SearchDescendants(term, useWildCards, searchProvider)); + } + + public DynamicPublishedContentList SearchChildren(string term, bool useWildCards = true, string searchProvider = null) + { + return new DynamicPublishedContentList(PublishedContent.SearchChildren(term, useWildCards, searchProvider)); + } + + public DynamicPublishedContentList Search(Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) + { + return new DynamicPublishedContentList(PublishedContent.Search(criteria, searchProvider)); + } + + #endregion + + #region IPublishedContent extension methods - AsDynamic + + public dynamic AsDynamic() + { + return this; + } + + public dynamic AsDynamicOrNull() + { + return this; + } + + #endregion + + #region IPublishedContente extension methods - ContentSet + + public int Position() + { + return Index(); + } + + public int Index() + { + return PublishedContent.GetIndex(); + } + + #endregion + + #region IPublishedContent extension methods - IsSomething: misc + + public bool Visible + { + get { return PublishedContent.IsVisible(); } + } + + public bool IsVisible() + { + return PublishedContent.IsVisible(); + } public bool IsDocumentType(string docTypeAlias) { - return this.PublishedContent.IsDocumentType(docTypeAlias); + return PublishedContent.IsDocumentType(docTypeAlias); } public bool IsNull(string alias, bool recursive) { - return this.PublishedContent.IsNull(alias, recursive); + return PublishedContent.IsNull(alias, recursive); } + public bool IsNull(string alias) { - return this.PublishedContent.IsNull(alias, false); + return PublishedContent.IsNull(alias, false); } - public bool IsFirst() + + #endregion + + #region IPublishedContent extension methods - IsSomething: position in set + + public bool IsFirst() { - return this.PublishedContent.IsFirst(); + return PublishedContent.IsFirst(); } + public HtmlString IsFirst(string valueIfTrue) { - return this.PublishedContent.IsFirst(valueIfTrue); + return PublishedContent.IsFirst(valueIfTrue); } + public HtmlString IsFirst(string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsFirst(valueIfTrue, valueIfFalse); + return PublishedContent.IsFirst(valueIfTrue, valueIfFalse); } + public bool IsNotFirst() { - return this.PublishedContent.IsNotFirst(); + return PublishedContent.IsNotFirst(); } + public HtmlString IsNotFirst(string valueIfTrue) { - return this.PublishedContent.IsNotFirst(valueIfTrue); + return PublishedContent.IsNotFirst(valueIfTrue); } + public HtmlString IsNotFirst(string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsNotFirst(valueIfTrue, valueIfFalse); + return PublishedContent.IsNotFirst(valueIfTrue, valueIfFalse); } + public bool IsPosition(int index) { - return this.PublishedContent.IsPosition(index); + return PublishedContent.IsPosition(index); } + public HtmlString IsPosition(int index, string valueIfTrue) { - return this.PublishedContent.IsPosition(index, valueIfTrue); + return PublishedContent.IsPosition(index, valueIfTrue); } + public HtmlString IsPosition(int index, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsPosition(index, valueIfTrue, valueIfFalse); + return PublishedContent.IsPosition(index, valueIfTrue, valueIfFalse); } + public bool IsModZero(int modulus) { - return this.PublishedContent.IsModZero(modulus); + return PublishedContent.IsModZero(modulus); } + public HtmlString IsModZero(int modulus, string valueIfTrue) { - return this.PublishedContent.IsModZero(modulus, valueIfTrue); + return PublishedContent.IsModZero(modulus, valueIfTrue); } + public HtmlString IsModZero(int modulus, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsModZero(modulus, valueIfTrue, valueIfFalse); + return PublishedContent.IsModZero(modulus, valueIfTrue, valueIfFalse); } public bool IsNotModZero(int modulus) { - return this.PublishedContent.IsNotModZero(modulus); + return PublishedContent.IsNotModZero(modulus); } + public HtmlString IsNotModZero(int modulus, string valueIfTrue) { - return this.PublishedContent.IsNotModZero(modulus, valueIfTrue); + return PublishedContent.IsNotModZero(modulus, valueIfTrue); } + public HtmlString IsNotModZero(int modulus, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsNotModZero(modulus, valueIfTrue, valueIfFalse); + return PublishedContent.IsNotModZero(modulus, valueIfTrue, valueIfFalse); } + public bool IsNotPosition(int index) { - return this.PublishedContent.IsNotPosition(index); + return PublishedContent.IsNotPosition(index); } + public HtmlString IsNotPosition(int index, string valueIfTrue) { - return this.PublishedContent.IsNotPosition(index, valueIfTrue); + return PublishedContent.IsNotPosition(index, valueIfTrue); } + public HtmlString IsNotPosition(int index, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsNotPosition(index, valueIfTrue, valueIfFalse); + return PublishedContent.IsNotPosition(index, valueIfTrue, valueIfFalse); } + public bool IsLast() { - return this.PublishedContent.IsLast(); + return PublishedContent.IsLast(); } + public HtmlString IsLast(string valueIfTrue) { - return this.PublishedContent.IsLast(valueIfTrue); + return PublishedContent.IsLast(valueIfTrue); } + public HtmlString IsLast(string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsLast(valueIfTrue, valueIfFalse); + return PublishedContent.IsLast(valueIfTrue, valueIfFalse); } + public bool IsNotLast() { - return this.PublishedContent.IsNotLast(); + return PublishedContent.IsNotLast(); } + public HtmlString IsNotLast(string valueIfTrue) { - return this.PublishedContent.IsNotLast(valueIfTrue); + return PublishedContent.IsNotLast(valueIfTrue); } + public HtmlString IsNotLast(string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsNotLast(valueIfTrue, valueIfFalse); + return PublishedContent.IsNotLast(valueIfTrue, valueIfFalse); } + public bool IsEven() { - return this.PublishedContent.IsEven(); + return PublishedContent.IsEven(); } + public HtmlString IsEven(string valueIfTrue) { - return this.PublishedContent.IsEven(valueIfTrue); + return PublishedContent.IsEven(valueIfTrue); } + public HtmlString IsEven(string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsEven(valueIfTrue, valueIfFalse); + return PublishedContent.IsEven(valueIfTrue, valueIfFalse); } + public bool IsOdd() { - return this.PublishedContent.IsOdd(); + return PublishedContent.IsOdd(); } + public HtmlString IsOdd(string valueIfTrue) { - return this.PublishedContent.IsOdd(valueIfTrue); + return PublishedContent.IsOdd(valueIfTrue); } + public HtmlString IsOdd(string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsOdd(valueIfTrue, valueIfFalse); + return PublishedContent.IsOdd(valueIfTrue, valueIfFalse); } - public bool IsEqual(DynamicPublishedContent other) + + #endregion + + #region IPublishedContent extension methods - IsSomething: equality + + public bool IsEqual(DynamicPublishedContent other) { - return this.PublishedContent.IsEqual(other); + return PublishedContent.IsEqual(other); } + public HtmlString IsEqual(DynamicPublishedContent other, string valueIfTrue) { - return this.PublishedContent.IsEqual(other, valueIfTrue); + return PublishedContent.IsEqual(other, valueIfTrue); } + public HtmlString IsEqual(DynamicPublishedContent other, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsEqual(other, valueIfTrue, valueIfFalse); + return PublishedContent.IsEqual(other, valueIfTrue, valueIfFalse); } + public bool IsNotEqual(DynamicPublishedContent other) { - return this.PublishedContent.IsNotEqual(other); + return PublishedContent.IsNotEqual(other); } + public HtmlString IsNotEqual(DynamicPublishedContent other, string valueIfTrue) { - return this.PublishedContent.IsNotEqual(other, valueIfTrue); + return PublishedContent.IsNotEqual(other, valueIfTrue); } + public HtmlString IsNotEqual(DynamicPublishedContent other, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsNotEqual(other, valueIfTrue, valueIfFalse); + return PublishedContent.IsNotEqual(other, valueIfTrue, valueIfFalse); } - public bool IsDescendant(DynamicPublishedContent other) + + #endregion + + #region IPublishedContent extension methods - IsSomething: ancestors and descendants + + public bool IsDescendant(DynamicPublishedContent other) { - return this.PublishedContent.IsDescendant(other); + return PublishedContent.IsDescendant(other); } + public HtmlString IsDescendant(DynamicPublishedContent other, string valueIfTrue) { - return this.PublishedContent.IsDescendant(other, valueIfTrue); + return PublishedContent.IsDescendant(other, valueIfTrue); } + public HtmlString IsDescendant(DynamicPublishedContent other, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsDescendant(other, valueIfTrue, valueIfFalse); + return PublishedContent.IsDescendant(other, valueIfTrue, valueIfFalse); } + public bool IsDescendantOrSelf(DynamicPublishedContent other) { - return this.PublishedContent.IsDescendantOrSelf(other); + return PublishedContent.IsDescendantOrSelf(other); } + public HtmlString IsDescendantOrSelf(DynamicPublishedContent other, string valueIfTrue) { - return this.PublishedContent.IsDescendantOrSelf(other, valueIfTrue); + return PublishedContent.IsDescendantOrSelf(other, valueIfTrue); } + public HtmlString IsDescendantOrSelf(DynamicPublishedContent other, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsDescendantOrSelf(other, valueIfTrue, valueIfFalse); + return PublishedContent.IsDescendantOrSelf(other, valueIfTrue, valueIfFalse); } + public bool IsAncestor(DynamicPublishedContent other) { - return this.PublishedContent.IsAncestor(other); + return PublishedContent.IsAncestor(other); } + public HtmlString IsAncestor(DynamicPublishedContent other, string valueIfTrue) { - return this.PublishedContent.IsAncestor(other, valueIfTrue); + return PublishedContent.IsAncestor(other, valueIfTrue); } + public HtmlString IsAncestor(DynamicPublishedContent other, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsAncestor(other, valueIfTrue, valueIfFalse); + return PublishedContent.IsAncestor(other, valueIfTrue, valueIfFalse); } + public bool IsAncestorOrSelf(DynamicPublishedContent other) { - return this.PublishedContent.IsAncestorOrSelf(other); + return PublishedContent.IsAncestorOrSelf(other); } + public HtmlString IsAncestorOrSelf(DynamicPublishedContent other, string valueIfTrue) { - return this.PublishedContent.IsAncestorOrSelf(other, valueIfTrue); + return PublishedContent.IsAncestorOrSelf(other, valueIfTrue); } + public HtmlString IsAncestorOrSelf(DynamicPublishedContent other, string valueIfTrue, string valueIfFalse) { - return this.PublishedContent.IsAncestorOrSelf(other, valueIfTrue, valueIfFalse); - } + return PublishedContent.IsAncestorOrSelf(other, valueIfTrue, valueIfFalse); + } + #endregion - #region Traversal + // all these methods wrap whatever PublishedContent returns in a new + // DynamicPublishedContentList, for dynamic usage. + + #region Ancestors + + public DynamicPublishedContentList Ancestors(int level) + { + return new DynamicPublishedContentList(PublishedContent.Ancestors(level)); + } + + public DynamicPublishedContentList Ancestors(string contentTypeAlias) + { + return new DynamicPublishedContentList(PublishedContent.Ancestors(contentTypeAlias)); + } + + public DynamicPublishedContentList Ancestors() + { + return new DynamicPublishedContentList(PublishedContent.Ancestors()); + } + + public DynamicPublishedContentList Ancestors(Func func) + { + return new DynamicPublishedContentList(PublishedContent.AncestorsOrSelf(false, func)); + } + + public DynamicPublishedContent AncestorOrSelf() + { + return PublishedContent.AncestorOrSelf().AsDynamicOrNull(); + } + + public DynamicPublishedContent AncestorOrSelf(int level) + { + return PublishedContent.AncestorOrSelf(level).AsDynamicOrNull(); + } + + public DynamicPublishedContent AncestorOrSelf(string contentTypeAlias) + { + return PublishedContent.AncestorOrSelf(contentTypeAlias).AsDynamicOrNull(); + } + + public DynamicPublishedContent AncestorOrSelf(Func func) + { + return PublishedContent.AncestorsOrSelf(true, func).FirstOrDefault().AsDynamicOrNull(); + } + + public DynamicPublishedContentList AncestorsOrSelf(Func func) + { + return new DynamicPublishedContentList(PublishedContent.AncestorsOrSelf(true, func)); + } + + public DynamicPublishedContentList AncestorsOrSelf() + { + return new DynamicPublishedContentList(PublishedContent.AncestorsOrSelf()); + } + + public DynamicPublishedContentList AncestorsOrSelf(string contentTypeAlias) + { + return new DynamicPublishedContentList(PublishedContent.AncestorsOrSelf(contentTypeAlias)); + } + + public DynamicPublishedContentList AncestorsOrSelf(int level) + { + return new DynamicPublishedContentList(PublishedContent.AncestorsOrSelf(level)); + } + + #endregion + + #region Descendants + + public DynamicPublishedContentList Descendants(string contentTypeAlias) + { + return new DynamicPublishedContentList(PublishedContent.Descendants(contentTypeAlias)); + } + public DynamicPublishedContentList Descendants(int level) + { + return new DynamicPublishedContentList(PublishedContent.Descendants(level)); + } + public DynamicPublishedContentList Descendants() + { + return new DynamicPublishedContentList(PublishedContent.Descendants()); + } + public DynamicPublishedContentList DescendantsOrSelf(int level) + { + return new DynamicPublishedContentList(PublishedContent.DescendantsOrSelf(level)); + } + public DynamicPublishedContentList DescendantsOrSelf(string contentTypeAlias) + { + return new DynamicPublishedContentList(PublishedContent.DescendantsOrSelf(contentTypeAlias)); + } + public DynamicPublishedContentList DescendantsOrSelf() + { + return new DynamicPublishedContentList(PublishedContent.DescendantsOrSelf()); + } + + #endregion + + #region Traversal + public DynamicPublishedContent Up() { - return Umbraco.Web.PublishedContentExtensions.Up(this).AsDynamicPublishedContent(); + return PublishedContent.Up().AsDynamicOrNull(); } + public DynamicPublishedContent Up(int number) { - return Umbraco.Web.PublishedContentExtensions.Up(this, number).AsDynamicPublishedContent(); + return PublishedContent.Up(number).AsDynamicOrNull(); } - public DynamicPublishedContent Up(string nodeTypeAlias) + + public DynamicPublishedContent Up(string contentTypeAlias) { - return Umbraco.Web.PublishedContentExtensions.Up(this, nodeTypeAlias).AsDynamicPublishedContent(); + return PublishedContent.Up(contentTypeAlias).AsDynamicOrNull(); } + public DynamicPublishedContent Down() { - return Umbraco.Web.PublishedContentExtensions.Down(this).AsDynamicPublishedContent(); + return PublishedContent.Down().AsDynamicOrNull(); } + public DynamicPublishedContent Down(int number) { - return Umbraco.Web.PublishedContentExtensions.Down(this, number).AsDynamicPublishedContent(); + return PublishedContent.Down(number).AsDynamicOrNull(); } - public DynamicPublishedContent Down(string nodeTypeAlias) + + public DynamicPublishedContent Down(string contentTypeAlias) { - return Umbraco.Web.PublishedContentExtensions.Down(this, nodeTypeAlias).AsDynamicPublishedContent(); + return PublishedContent.Down(contentTypeAlias).AsDynamicOrNull(); } + public DynamicPublishedContent Next() { - return Umbraco.Web.PublishedContentExtensions.Next(this).AsDynamicPublishedContent(); + return PublishedContent.Next().AsDynamicOrNull(); } + public DynamicPublishedContent Next(int number) { - return Umbraco.Web.PublishedContentExtensions.Next(this, number).AsDynamicPublishedContent(); + return PublishedContent.Next(number).AsDynamicOrNull(); } - public DynamicPublishedContent Next(string nodeTypeAlias) + + public DynamicPublishedContent Next(string contentTypeAlias) { - return Umbraco.Web.PublishedContentExtensions.Next(this, nodeTypeAlias).AsDynamicPublishedContent(); + return PublishedContent.Next(contentTypeAlias).AsDynamicOrNull(); } public DynamicPublishedContent Previous() { - return Umbraco.Web.PublishedContentExtensions.Previous(this).AsDynamicPublishedContent(); + return PublishedContent.Previous().AsDynamicOrNull(); } + public DynamicPublishedContent Previous(int number) { - return Umbraco.Web.PublishedContentExtensions.Previous(this, number).AsDynamicPublishedContent(); + return PublishedContent.Previous(number).AsDynamicOrNull(); } - public DynamicPublishedContent Previous(string nodeTypeAlias) + + public DynamicPublishedContent Previous(string contentTypeAlias) { - return Umbraco.Web.PublishedContentExtensions.Previous(this, nodeTypeAlias).AsDynamicPublishedContent(); + return PublishedContent.Previous(contentTypeAlias).AsDynamicOrNull(); } + public DynamicPublishedContent Sibling(int number) { - return Umbraco.Web.PublishedContentExtensions.Previous(this, number).AsDynamicPublishedContent(); + return PublishedContent.Previous(number).AsDynamicOrNull(); } - public DynamicPublishedContent Sibling(string nodeTypeAlias) + + public DynamicPublishedContent Sibling(string contentTypeAlias) { - return Umbraco.Web.PublishedContentExtensions.Previous(this, nodeTypeAlias).AsDynamicPublishedContent(); + return PublishedContent.Previous(contentTypeAlias).AsDynamicOrNull(); } + #endregion - #region Ancestors, Descendants and Parent - #region Ancestors - public DynamicPublishedContentList Ancestors(int level) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Ancestors(this, level)); - } - public DynamicPublishedContentList Ancestors(string nodeTypeAlias) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Ancestors(this, nodeTypeAlias)); - } - public DynamicPublishedContentList Ancestors() - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Ancestors(this)); - } - public DynamicPublishedContentList Ancestors(Func func) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Ancestors(this, func)); - } - public DynamicPublishedContent AncestorOrSelf() - { - return Umbraco.Web.PublishedContentExtensions.AncestorOrSelf(this).AsDynamicPublishedContent(); - } - public DynamicPublishedContent AncestorOrSelf(int level) - { - return Umbraco.Web.PublishedContentExtensions.AncestorOrSelf(this, level).AsDynamicPublishedContent(); - } - public DynamicPublishedContent AncestorOrSelf(string nodeTypeAlias) - { - return Umbraco.Web.PublishedContentExtensions.AncestorOrSelf(this, nodeTypeAlias).AsDynamicPublishedContent(); - } - public DynamicPublishedContent AncestorOrSelf(Func func) - { - return Umbraco.Web.PublishedContentExtensions.AncestorOrSelf(this, func).AsDynamicPublishedContent(); - } - public DynamicPublishedContentList AncestorsOrSelf(Func func) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.AncestorsOrSelf(this, func)); - } - public DynamicPublishedContentList AncestorsOrSelf() - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.AncestorsOrSelf(this)); - } - public DynamicPublishedContentList AncestorsOrSelf(string nodeTypeAlias) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.AncestorsOrSelf(this, nodeTypeAlias)); - } - public DynamicPublishedContentList AncestorsOrSelf(int level) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.AncestorsOrSelf(this, level)); - } - #endregion - #region Descendants - public DynamicPublishedContentList Descendants(string nodeTypeAlias) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Descendants(this, nodeTypeAlias)); - } - public DynamicPublishedContentList Descendants(int level) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Descendants(this, level)); - } - public DynamicPublishedContentList Descendants() - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.Descendants(this)); - } - public DynamicPublishedContentList DescendantsOrSelf(int level) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.DescendantsOrSelf(this, level)); - } - public DynamicPublishedContentList DescendantsOrSelf(string nodeTypeAlias) - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.DescendantsOrSelf(this, nodeTypeAlias)); - } - public DynamicPublishedContentList DescendantsOrSelf() - { - return new DynamicPublishedContentList( - Umbraco.Web.PublishedContentExtensions.DescendantsOrSelf(this)); - } - #endregion + #region Parent public DynamicPublishedContent Parent { get { - if (PublishedContent.Parent != null) - { - return PublishedContent.Parent.AsDynamicPublishedContent(); - } - if (PublishedContent != null && PublishedContent.Id == 0) - { - return this; - } - return null; + return PublishedContent.Parent != null ? PublishedContent.Parent.AsDynamicOrNull() : null; } } #endregion - #region Where + #region Children - public HtmlString Where(string predicate, string valueIfTrue) + // we want to cache the dynamic list of children here + // whether PublishedContent.Children itself is cached, is not our concern + + private DynamicPublishedContentList _children; + + public DynamicPublishedContentList Children + { + get { return _children ?? (_children = new DynamicPublishedContentList(PublishedContent.Children)); } + } + + #endregion + + // should probably cleanup what's below + + #region Where + + public HtmlString Where(string predicate, string valueIfTrue) { return Where(predicate, valueIfTrue, string.Empty); } @@ -1248,6 +1314,5 @@ namespace Umbraco.Web.Models } #endregion - - } + } } diff --git a/src/Umbraco.Web/Models/DynamicPublishedContentList.cs b/src/Umbraco.Web/Models/DynamicPublishedContentList.cs index d937a31bbe..94ce82799c 100644 --- a/src/Umbraco.Web/Models/DynamicPublishedContentList.cs +++ b/src/Umbraco.Web/Models/DynamicPublishedContentList.cs @@ -7,66 +7,107 @@ using Umbraco.Core.Dynamics; using System.Collections; using System.Reflection; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Dynamics; namespace Umbraco.Web.Models { /// - /// A collection of DynamicPublishedContent items + /// Represents a collection of DynamicPublishedContent items. /// - /// - /// Implements many of the dynamic methods required for execution against this list. It also ensures - /// that the correct OwnersCollection properties is assigned to the underlying PublishedContentBase object - /// of the DynamicPublishedContent item (so long as the IPublishedContent item is actually PublishedContentBase). - /// All relates to this issue here: http://issues.umbraco.org/issue/U4-1797 - /// public class DynamicPublishedContentList : DynamicObject, IEnumerable, IEnumerable { - internal List Items { get; set; } + private readonly List _content; + private readonly PublishedContentSet _contentSet; + internal readonly List Items; + + #region Constructor public DynamicPublishedContentList() { + _content = new List(); + _contentSet = new PublishedContentSet(_content); Items = new List(); } - public DynamicPublishedContentList(IEnumerable items) - { - var list = items.ToList(); - //set the owners list for each item - list.ForEach(x => SetOwnersList(x, this)); - Items = list; - } public DynamicPublishedContentList(IEnumerable items) { - var list = items.Select(x => new DynamicPublishedContent(x)).ToList(); - //set the owners list for each item - list.ForEach(x => SetOwnersList(x, this)); - Items = list; + _content = items.ToList(); + _contentSet = new PublishedContentSet(_content); + Items = _contentSet.Select(x => new DynamicPublishedContent(x, this)).ToList(); } - private static void SetOwnersList(IPublishedContent content, IEnumerable list) + public DynamicPublishedContentList(IEnumerable items) { - var publishedContentBase = content as IOwnerCollectionAware; - if (publishedContentBase != null) - { - publishedContentBase.OwnersCollection = list; - } + _content = items.Select(x => x.PublishedContent).ToList(); + _contentSet = new PublishedContentSet(_content); + Items = _contentSet.Select(x => new DynamicPublishedContent(x, this)).ToList(); } + #endregion + + #region ContentSet + + // so we are ~compatible with strongly typed syntax + public DynamicPublishedContentList ToContentSet() + { + return this; + } + + #endregion + + #region IList (well, part of it) + + /// + /// Adds an item to the collection. + /// + /// The item to add. + public void Add(DynamicPublishedContent dynamicContent) + { + var content = dynamicContent.PublishedContent; + _content.Add(content); + _contentSet.SourceChanged(); + + var setContent = _contentSet.MapContent(content); + Items.Add(new DynamicPublishedContent(setContent, this)); + } + + /// + /// Removes an item from the collection. + /// + /// The item to remove. + public void Remove(DynamicPublishedContent dynamicContent) + { + if (Items.Contains(dynamicContent) == false) return; + Items.Remove(dynamicContent); + _content.Remove(dynamicContent.PublishedContent); + _contentSet.SourceChanged(); + } + + #endregion + + #region DynamicObject + + // because we want to return DynamicNull and not null, we need to implement the index property + // via the dynamic getter and not natively - otherwise it's not possible to return DynamicNull + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) { - int index = (int)indexes[0]; - try - { - result = this.Items.ElementAt(index); - return true; - } - catch (IndexOutOfRangeException) - { - result = new DynamicNull(); - return true; - } + result = DynamicNull.Null; + + if (indexes.Length != 1) + return false; + + var index = indexes[0] as int?; + if (index.HasValue == false) + return false; + + if (index >= 0 && index < Items.Count) + result = Items[index.Value]; + + return true; } + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { //TODO: Nowhere here are we checking if args is the correct length! @@ -86,7 +127,7 @@ namespace Umbraco.Web.Models { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] {} : args.Skip(1).ToArray(); - var single = this.Single(predicate, values); + var single = Single(predicate, values); result = new DynamicPublishedContent(single); return true; } @@ -94,9 +135,9 @@ namespace Umbraco.Web.Models { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); - var single = this.SingleOrDefault(predicate, values); + var single = SingleOrDefault(predicate, values); if (single == null) - result = new DynamicNull(); + result = DynamicNull.Null; else result = new DynamicPublishedContent(single); return true; @@ -105,7 +146,7 @@ namespace Umbraco.Web.Models { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); - var first = this.First(predicate, values); + var first = First(predicate, values); result = new DynamicPublishedContent(first); return true; } @@ -113,9 +154,9 @@ namespace Umbraco.Web.Models { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); - var first = this.FirstOrDefault(predicate, values); + var first = FirstOrDefault(predicate, values); if (first == null) - result = new DynamicNull(); + result = DynamicNull.Null; else result = new DynamicPublishedContent(first); return true; @@ -124,7 +165,7 @@ namespace Umbraco.Web.Models { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); - var last = this.Last(predicate, values); + var last = Last(predicate, values); result = new DynamicPublishedContent(last); return true; } @@ -132,9 +173,9 @@ namespace Umbraco.Web.Models { string predicate = firstArg == null ? "" : firstArg.ToString(); var values = predicate.IsNullOrWhiteSpace() ? new object[] { } : args.Skip(1).ToArray(); - var last = this.LastOrDefault(predicate, values); + var last = LastOrDefault(predicate, values); if (last == null) - result = new DynamicNull(); + result = DynamicNull.Null; else result = new DynamicPublishedContent(last); return true; @@ -145,14 +186,14 @@ namespace Umbraco.Web.Models var values = args.Skip(1).ToArray(); //TODO: We are pre-resolving the where into a ToList() here which will have performance impacts if there where clauses // are nested! We should somehow support an QueryableDocumentList! - result = new DynamicPublishedContentList(this.Where(predicate, values).ToList()); + result = new DynamicPublishedContentList(Where(predicate, values).ToList()); return true; } if (name == "OrderBy") { //TODO: We are pre-resolving the where into a ToList() here which will have performance impacts if there where clauses // are nested! We should somehow support an QueryableDocumentList! - result = new DynamicPublishedContentList(this.OrderBy(firstArg.ToString()).ToList()); + result = new DynamicPublishedContentList(OrderBy(firstArg.ToString()).ToList()); return true; } if (name == "Take") @@ -167,24 +208,24 @@ namespace Umbraco.Web.Models } if (name == "InGroupsOf") { - int groupSize = 0; + int groupSize; if (int.TryParse(firstArg.ToString(), out groupSize)) { result = InGroupsOf(groupSize); return true; } - result = new DynamicNull(); + result = DynamicNull.Null; return true; } if (name == "GroupedInto") { - int groupCount = 0; + int groupCount; if (int.TryParse(firstArg.ToString(), out groupCount)) { result = GroupedInto(groupCount); return true; } - result = new DynamicNull(); + result = DynamicNull.Null; return true; } if (name == "GroupBy") @@ -199,14 +240,20 @@ namespace Umbraco.Web.Models } if (name == "Union") { - if ((firstArg as IEnumerable) != null) + // check DynamicPublishedContentList before IEnumerable<...> because DynamicPublishedContentList + // is IEnumerable<...> so ... the check on DynamicPublishedContentList would never be reached. + + var firstArgAsDynamicPublishedContentList = firstArg as DynamicPublishedContentList; + if (firstArgAsDynamicPublishedContentList != null) { - result = new DynamicPublishedContentList(this.Items.Union(firstArg as IEnumerable)); + result = new DynamicPublishedContentList(Items.Union((firstArgAsDynamicPublishedContentList).Items)); return true; } - if ((firstArg as DynamicPublishedContentList) != null) + + var firstArgAsIEnumerable = firstArg as IEnumerable; + if (firstArgAsIEnumerable != null) { - result = new DynamicPublishedContentList(this.Items.Union((firstArg as DynamicPublishedContentList).Items)); + result = new DynamicPublishedContentList(Items.Union(firstArgAsIEnumerable)); return true; } } @@ -214,7 +261,7 @@ namespace Umbraco.Web.Models { if ((firstArg as IEnumerable) != null) { - result = new DynamicPublishedContentList(this.Items.Except(firstArg as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); + result = new DynamicPublishedContentList(Items.Except(firstArg as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); return true; } } @@ -222,13 +269,13 @@ namespace Umbraco.Web.Models { if ((firstArg as IEnumerable) != null) { - result = new DynamicPublishedContentList(this.Items.Intersect(firstArg as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); + result = new DynamicPublishedContentList(Items.Intersect(firstArg as IEnumerable, new DynamicPublishedContentIdEqualityComparer())); return true; } } if (name == "Distinct") { - result = new DynamicPublishedContentList(this.Items.Distinct(new DynamicPublishedContentIdEqualityComparer())); + result = new DynamicPublishedContentList(Items.Distinct(new DynamicPublishedContentIdEqualityComparer())); return true; } if (name == "Pluck" || name == "Select") @@ -275,7 +322,7 @@ namespace Umbraco.Web.Models if (attempt.Result.Reason == DynamicInstanceHelper.TryInvokeMemberSuccessReason.FoundExtensionMethod && attempt.Exception != null && attempt.Exception is TargetInvocationException) { - result = new DynamicNull(); + result = DynamicNull.Null; return true; } @@ -283,6 +330,11 @@ namespace Umbraco.Web.Models return false; } + + #endregion + + #region Linq and stuff + private T Aggregate(IEnumerable data, string name) where T : struct { switch (name) @@ -323,7 +375,7 @@ namespace Umbraco.Web.Models object firstItem = query.FirstOrDefault(); if (firstItem == null) { - result = new DynamicNull(); + result = DynamicNull.Null; } else { @@ -465,7 +517,7 @@ namespace Umbraco.Web.Models } public IQueryable OrderBy(string key) { - return ((IQueryable)Items.AsQueryable()).OrderBy(key, () => typeof(DynamicPublishedContentListOrdering)); + return ((IQueryable)Items.AsQueryable()).OrderBy(key, () => typeof(DynamicPublishedContentListOrdering)); } public DynamicGrouping GroupBy(string key) { @@ -474,10 +526,9 @@ namespace Umbraco.Web.Models } public DynamicGrouping GroupedInto(int groupCount) { - int groupSize = (int)Math.Ceiling(((decimal)Items.Count() / groupCount)); + var groupSize = (int)Math.Ceiling(((decimal)Items.Count() / groupCount)); return new DynamicGrouping( - this - .Items + Items .Select((node, index) => new KeyValuePair(index, node)) .GroupBy(kv => (object)(kv.Key / groupSize)) .Select(item => new Grouping() @@ -489,8 +540,7 @@ namespace Umbraco.Web.Models public DynamicGrouping InGroupsOf(int groupSize) { return new DynamicGrouping( - this - .Items + Items .Select((node, index) => new KeyValuePair(index, node)) .GroupBy(kv => (object)(kv.Key / groupSize)) .Select(item => new Grouping() @@ -506,38 +556,24 @@ namespace Umbraco.Web.Models return DynamicQueryable.Select(Items.AsQueryable(), predicate, values); } - /// - /// Allows the adding of an item from the collection - /// - /// - public void Add(DynamicPublishedContent publishedContent) - { - SetOwnersList(publishedContent, this); - this.Items.Add(publishedContent); - } + #endregion + + #region Dynamic - /// - /// Allows the removal of an item from the collection - /// - /// - public void Remove(DynamicPublishedContent publishedContent) - { - if (this.Items.Contains(publishedContent)) - { - //set owners list to null - SetOwnersList(publishedContent, null); - this.Items.Remove(publishedContent); - } - } public bool IsNull() { return false; } + public bool HasValue() { return true; } + #endregion + + #region Enumerate inner IPublishedContent items + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); @@ -551,6 +587,8 @@ namespace Umbraco.Web.Models IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); - } + } + + #endregion } } diff --git a/src/Umbraco.Web/Models/IOwnerCollectionAware.cs b/src/Umbraco.Web/Models/IOwnerCollectionAware.cs deleted file mode 100644 index a224f5c227..0000000000 --- a/src/Umbraco.Web/Models/IOwnerCollectionAware.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Web.Models -{ - /// - /// An interface describing that the object should be aware of it's containing collection - /// - internal interface IOwnerCollectionAware - { - IEnumerable OwnersCollection { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 12501c6fac..c0a58ffbdf 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -1,62 +1,36 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; -using System.Linq; -using System.Text; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Web.Templates; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models { /// - /// An abstract base class to use for IPublishedContent which ensures that the Url and Indexed property return values - /// are consistently returned. + /// Provide an abstract base class for IPublishedContent implementations. /// - /// - /// This also ensures that we have an OwnersCollection property so that the IsFirst/IsLast/Index helper methods work - /// when referenced inside the result of a collection. http://issues.umbraco.org/issue/U4-1797 - /// + /// This base class does which (a) consitently resolves and caches the Url, (b) provides an implementation + /// for this[alias], and (c) provides basic content set management. [DebuggerDisplay("Content Id: {Id}, Name: {Name}")] - public abstract class PublishedContentBase : IPublishedContent, IOwnerCollectionAware - { - private string _url; - private readonly Dictionary _resolvePropertyValues = new Dictionary(); - private IEnumerable _ownersCollection; + public abstract class PublishedContentBase : IPublishedContent + { + #region Content - /// - /// Need to get/set the owner collection when an item is returned from the result set of a query - /// - /// - /// Based on this issue here: http://issues.umbraco.org/issue/U4-1797 - /// - IEnumerable IOwnerCollectionAware.OwnersCollection - { - get - { - //if the owners collection is null, we'll default to it's siblings - if (_ownersCollection == null) - { - //get the root docs if parent is null - _ownersCollection = this.Siblings(); - } - return _ownersCollection; - } - set { _ownersCollection = value; } - } + private string _url; /// - /// Returns the Url for this content item + /// Gets the url of the content. /// /// - /// If this item type is media, the Url that is returned is the Url computed by the NiceUrlProvider, otherwise if it is media - /// the Url returned is the value found in the 'umbracoFile' property. + /// If this content is Content, the url that is returned is the one computed by the NiceUrlProvider, otherwise if + /// this content is Media, the url returned is the value found in the 'umbracoFile' property. /// public virtual string Url { get { + // should be thread-safe although it won't prevent url from being resolved more than once if (_url != null) return _url; @@ -64,20 +38,21 @@ namespace Umbraco.Web.Models { case PublishedItemType.Content: if (UmbracoContext.Current == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item with a null UmbracoContext.Current reference"); + throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current is null."); if (UmbracoContext.Current.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item with a null UmbracoContext.Current.NiceUrlProvider reference"); - _url= UmbracoContext.Current.UrlProvider.GetUrl(this.Id); + throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); + _url= UmbracoContext.Current.UrlProvider.GetUrl(Id); break; case PublishedItemType.Media: var prop = GetProperty(Constants.Conventions.Media.File); if (prop == null) - throw new NotSupportedException("Cannot retreive a Url for a media item if there is no 'umbracoFile' property defined"); - _url = prop.Value.ToString(); + throw new NotSupportedException("Cannot resolve a Url for a media item when there is no 'umbracoFile' property defined."); + _url = prop.ObjectValue.ToString(); break; default: throw new ArgumentOutOfRangeException(); } + return _url; } } @@ -99,32 +74,126 @@ namespace Umbraco.Web.Models public abstract DateTime UpdateDate { get; } public abstract Guid Version { get; } public abstract int Level { get; } - public abstract ICollection Properties { get; } - /// - /// Returns the property value for the property alias specified - /// - /// - /// - /// - /// Ensures that the value is executed through the IPropertyEditorValueConverters and that all internal links are are to date - /// - public virtual object this[string propertyAlias] + public abstract bool IsDraft { get; } + + public int GetIndex() + { + var index = this.Siblings().FindIndex(x => x.Id == Id); + if (index < 0) + throw new IndexOutOfRangeException("Could not find content in the content set."); + return index; + } + + #endregion + + #region Tree + + /// + /// Gets the parent of the content. + /// + public abstract IPublishedContent Parent { get; } + + /// + /// Gets the children of the content. + /// + /// Children are sorted by their sortOrder. + public abstract IEnumerable Children { get; } + + #endregion + + #region ContentSet + + public virtual IEnumerable ContentSet + { + // the default content set of a content is its siblings + get { return this.Siblings(); } + } + + #endregion + + #region ContentType + + public abstract PublishedContentType ContentType { get; } + + #endregion + + #region Properties + + /// + /// Gets the properties of the content. + /// + public abstract ICollection Properties { get; } + + /// + /// Gets the value of a property identified by its alias. + /// + /// The property alias. + /// The value of the property identified by the alias. + /// + /// If GetProperty(alias) is null then returns null else return GetProperty(alias).Value. + /// So if the property has no value, returns the default value for that property type. + /// This one is defined here really because we cannot define index extension methods, but all it should do is: + /// var p = GetProperty(alias); return p == null ? null : p.Value; and nothing else. + /// The recursive syntax (eg "_title") is _not_ supported here. + /// The alias is case-insensitive. + /// + public virtual object this[string alias] { get { - //check this instance's cache, this is better for performance because resolving a value can - //have performance impacts since it has to resolve Urls and IPropertyEditorValueConverter's as well. - if (_resolvePropertyValues.ContainsKey(propertyAlias)) - return _resolvePropertyValues[propertyAlias]; - _resolvePropertyValues.Add(propertyAlias, this.GetPropertyValue(propertyAlias)); - return _resolvePropertyValues[propertyAlias]; + // no cache here: GetProperty should be fast, and .Value cache should be managed by the property. + var property = GetProperty(alias); + return property == null ? null : property.ObjectValue; } } - public abstract IPublishedContentProperty GetProperty(string alias); - public abstract IPublishedContent Parent { get; } - public abstract IEnumerable Children { get; } + /// + /// Gets a property identified by its alias. + /// + /// The property alias. + /// The property identified by the alias. + /// + /// If no property with the specified alias exists, returns null. + /// The returned property may have no value (ie HasValue is false). + /// The alias is case-insensitive. + /// + public abstract IPublishedProperty GetProperty(string alias); + /// + /// Gets a property identified by its alias. + /// + /// The property alias. + /// A value indicating whether to navigate the tree upwards until a property with a value is found. + /// The property identified by the alias. + /// + /// Navigate the tree upwards and look for a property with that alias and with a value (ie HasValue is true). + /// If found, return the property. If no property with that alias is found, having a value or not, return null. Otherwise + /// return the first property that was found with the alias but had no value (ie HasValue is false). + /// The alias is case-insensitive. + /// + public virtual IPublishedProperty GetProperty(string alias, bool recurse) + { + var property = GetProperty(alias); + if (recurse == false) return property; + + IPublishedContent content = this; + var firstNonNullProperty = property; + while (content != null && (property == null || property.HasValue == false)) + { + content = content.Parent; + property = content == null ? null : content.GetProperty(alias); + if (firstNonNullProperty == null && property != null) firstNonNullProperty = property; + } + + // if we find a content with the property with a value, return that property + // if we find no content with the property, return null + // if we find a content with the property without a value, return that property + // have to save that first property while we look further up, hence firstNonNullProperty + + return property != null && property.HasValue ? property : firstNonNullProperty; + } + + #endregion } } diff --git a/src/Umbraco.Web/Models/XmlPublishedContent.cs b/src/Umbraco.Web/Models/XmlPublishedContent.cs deleted file mode 100644 index a0e1b10539..0000000000 --- a/src/Umbraco.Web/Models/XmlPublishedContent.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Xml; -using System.Xml.Serialization; -using System.Xml.XPath; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.Models -{ - - /// - /// Represents an IPublishedContent which is created based on an Xml structure - /// - [Serializable] - [XmlType(Namespace = "http://umbraco.org/webservices/")] - internal class XmlPublishedContent : PublishedContentBase - { - /// - /// Constructor - /// - /// - public XmlPublishedContent(XmlNode xmlNode) - { - _pageXmlNode = xmlNode; - InitializeStructure(); - Initialize(); - } - - /// - /// Constructor - /// - /// - /// - internal XmlPublishedContent(XmlNode xmlNode, bool disableInitializing) - { - _pageXmlNode = xmlNode; - InitializeStructure(); - if (!disableInitializing) - Initialize(); - } - - private bool _initialized = false; - private readonly ICollection _children = new Collection(); - private IPublishedContent _parent = null; - private int _id; - private int _template; - private string _name; - private string _docTypeAlias; - private int _docTypeId; - private string _writerName; - private string _creatorName; - private int _writerId; - private int _creatorId; - private string _urlName; - private string _path; - private DateTime _createDate; - private DateTime _updateDate; - private Guid _version; - private readonly Collection _properties = new Collection(); - private readonly XmlNode _pageXmlNode; - private int _sortOrder; - private int _level; - - public override IEnumerable Children - { - get - { - if (!_initialized) - Initialize(); - return _children.OrderBy(x => x.SortOrder); - } - } - - public override IPublishedContentProperty GetProperty(string alias) - { - return Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - } - - /// - /// returns 'Content' as the ItemType - /// - public override PublishedItemType ItemType - { - get { return PublishedItemType.Content; } - } - - public override IPublishedContent Parent - { - get - { - if (!_initialized) - Initialize(); - return _parent; - } - } - - public override int Id - { - get - { - if (!_initialized) - Initialize(); - return _id; - } - } - - public override int TemplateId - { - get - { - if (!_initialized) - Initialize(); - return _template; - } - } - - public override int SortOrder - { - get - { - if (!_initialized) - Initialize(); - return _sortOrder; - } - } - - public override string Name - { - get - { - if (!_initialized) - Initialize(); - return _name; - } - } - - public override string DocumentTypeAlias - { - get - { - if (!_initialized) - Initialize(); - return _docTypeAlias; - } - } - - public override int DocumentTypeId - { - get - { - if (!_initialized) - Initialize(); - return _docTypeId; - } - } - - public override string WriterName - { - get - { - if (!_initialized) - Initialize(); - return _writerName; - } - } - - public override string CreatorName - { - get - { - if (!_initialized) - Initialize(); - return _creatorName; - } - } - - public override int WriterId - { - get - { - if (!_initialized) - Initialize(); - return _writerId; - } - } - - public override int CreatorId - { - get - { - if (!_initialized) - Initialize(); - return _creatorId; - } - } - - - public override string Path - { - get - { - if (!_initialized) - Initialize(); - return _path; - } - } - - public override DateTime CreateDate - { - get - { - if (!_initialized) - Initialize(); - return _createDate; - } - } - - public override DateTime UpdateDate - { - get - { - if (!_initialized) - Initialize(); - return _updateDate; - } - } - - public override Guid Version - { - get - { - if (!_initialized) - Initialize(); - return _version; - } - } - - public override string UrlName - { - get - { - if (!_initialized) - Initialize(); - return _urlName; - } - } - - public override int Level - { - get - { - if (!_initialized) - Initialize(); - return _level; - } - } - - public override ICollection Properties - { - get - { - if (!_initialized) - Initialize(); - return _properties; - } - } - - - private void InitializeStructure() - { - // Load parent if it exists and is a node - - if (_pageXmlNode != null && _pageXmlNode.SelectSingleNode("..") != null) - { - XmlNode parent = _pageXmlNode.SelectSingleNode(".."); - if (parent != null && (parent.Name == "node" || (parent.Attributes != null && parent.Attributes.GetNamedItem("isDoc") != null))) - _parent = new XmlPublishedContent(parent, true); - } - } - - private void Initialize() - { - if (_pageXmlNode != null) - { - _initialized = true; - if (_pageXmlNode.Attributes != null) - { - _id = int.Parse(_pageXmlNode.Attributes.GetNamedItem("id").Value); - if (_pageXmlNode.Attributes.GetNamedItem("template") != null) - _template = int.Parse(_pageXmlNode.Attributes.GetNamedItem("template").Value); - if (_pageXmlNode.Attributes.GetNamedItem("sortOrder") != null) - _sortOrder = int.Parse(_pageXmlNode.Attributes.GetNamedItem("sortOrder").Value); - if (_pageXmlNode.Attributes.GetNamedItem("nodeName") != null) - _name = _pageXmlNode.Attributes.GetNamedItem("nodeName").Value; - if (_pageXmlNode.Attributes.GetNamedItem("writerName") != null) - _writerName = _pageXmlNode.Attributes.GetNamedItem("writerName").Value; - if (_pageXmlNode.Attributes.GetNamedItem("urlName") != null) - _urlName = _pageXmlNode.Attributes.GetNamedItem("urlName").Value; - // Creatorname is new in 2.1, so published xml might not have it! - try - { - _creatorName = _pageXmlNode.Attributes.GetNamedItem("creatorName").Value; - } - catch - { - _creatorName = _writerName; - } - - //Added the actual userID, as a user cannot be looked up via full name only... - if (_pageXmlNode.Attributes.GetNamedItem("creatorID") != null) - _creatorId = int.Parse(_pageXmlNode.Attributes.GetNamedItem("creatorID").Value); - if (_pageXmlNode.Attributes.GetNamedItem("writerID") != null) - _writerId = int.Parse(_pageXmlNode.Attributes.GetNamedItem("writerID").Value); - - if (UmbracoConfiguration.Current.UmbracoSettings.Content.UseLegacyXmlSchema) - { - if (_pageXmlNode.Attributes.GetNamedItem("nodeTypeAlias") != null) - _docTypeAlias = _pageXmlNode.Attributes.GetNamedItem("nodeTypeAlias").Value; - } - else - { - _docTypeAlias = _pageXmlNode.Name; - } - - if (_pageXmlNode.Attributes.GetNamedItem("nodeType") != null) - _docTypeId = int.Parse(_pageXmlNode.Attributes.GetNamedItem("nodeType").Value); - if (_pageXmlNode.Attributes.GetNamedItem("path") != null) - _path = _pageXmlNode.Attributes.GetNamedItem("path").Value; - if (_pageXmlNode.Attributes.GetNamedItem("version") != null) - _version = new Guid(_pageXmlNode.Attributes.GetNamedItem("version").Value); - if (_pageXmlNode.Attributes.GetNamedItem("createDate") != null) - _createDate = DateTime.Parse(_pageXmlNode.Attributes.GetNamedItem("createDate").Value); - if (_pageXmlNode.Attributes.GetNamedItem("updateDate") != null) - _updateDate = DateTime.Parse(_pageXmlNode.Attributes.GetNamedItem("updateDate").Value); - if (_pageXmlNode.Attributes.GetNamedItem("level") != null) - _level = int.Parse(_pageXmlNode.Attributes.GetNamedItem("level").Value); - - } - - // load data - var dataXPath = UmbracoConfiguration.Current.UmbracoSettings.Content.UseLegacyXmlSchema ? "data" : "* [not(@isDoc)]"; - foreach (XmlNode n in _pageXmlNode.SelectNodes(dataXPath)) - _properties.Add(new XmlPublishedContentProperty(n)); - - // load children - var childXPath = UmbracoConfiguration.Current.UmbracoSettings.Content.UseLegacyXmlSchema ? "node" : "* [@isDoc]"; - var nav = _pageXmlNode.CreateNavigator(); - var expr = nav.Compile(childXPath); - expr.AddSort("@sortOrder", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number); - var iterator = nav.Select(expr); - while (iterator.MoveNext()) - { - _children.Add( - new XmlPublishedContent(((IHasXmlNode)iterator.Current).GetNode(), true) - ); - } - } - // else - // throw new ArgumentNullException("Node xml source is null"); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/XmlPublishedContentProperty.cs b/src/Umbraco.Web/Models/XmlPublishedContentProperty.cs deleted file mode 100644 index b900ca6ee8..0000000000 --- a/src/Umbraco.Web/Models/XmlPublishedContentProperty.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Xml; -using System.Xml.Serialization; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models; -using Umbraco.Web.Templates; - -namespace Umbraco.Web.Models -{ - - /// - /// Represents an IDocumentProperty which is created based on an Xml structure. - /// - [Serializable] - [XmlType(Namespace = "http://umbraco.org/webservices/")] - public class XmlPublishedContentProperty : IPublishedContentProperty - { - private readonly Guid _version; - private readonly string _alias; - private readonly string _value; - - public string Alias - { - get { return _alias; } - } - - private string _parsedValue; - - /// - /// Returns the value of a property from the XML cache - /// - /// - /// This ensures that the result has any {localLink} syntax parsed and that urls are resolved correctly. - /// This also ensures that the parsing is only done once as the result is cached in a private field of this object. - /// - public object Value - { - get - { - if (_parsedValue == null) - { - _parsedValue = TemplateUtilities.ResolveUrlsFromTextString( - TemplateUtilities.ParseInternalLinks( - _value)); - } - return _parsedValue; - } - } - - public Guid Version - { - get { return _version; } - } - - public XmlPublishedContentProperty() - { - - } - - public XmlPublishedContentProperty(XmlNode propertyXmlData) - { - if (propertyXmlData != null) - { - // For backward compatibility with 2.x (the version attribute has been removed from 3.0 data nodes) - if (propertyXmlData.Attributes.GetNamedItem("versionID") != null) - _version = new Guid(propertyXmlData.Attributes.GetNamedItem("versionID").Value); - _alias = UmbracoConfiguration.Current.UmbracoSettings.Content.UseLegacyXmlSchema ? - propertyXmlData.Attributes.GetNamedItem("alias").Value : - propertyXmlData.Name; - _value = XmlHelper.GetNodeValue(propertyXmlData); - } - else - throw new ArgumentNullException("Property xml source is null"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs new file mode 100644 index 0000000000..fd09dbcb2d --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Web; +using Umbraco.Core; +using Umbraco.Core.Macros; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// A value converter for TinyMCE that will ensure any macro content is rendered properly even when + /// used dynamically. + /// + // because that version of RTE converter parses {locallink} and executes macros, when going from + // data to source, its source value has to be cached at the request level, because we have no idea + // what the macros may depend on actually. An so, object and xpath need to follow... request, too. + // note: the TinyMceValueConverter is NOT inherited, so the PropertyValueCache attribute here is not + // actually required (since Request is default) but leave it here to be absolutely explicit. + [PropertyValueType(typeof(IHtmlString))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Request)] + public class RteMacroRenderingValueConverter : TinyMceValueConverter + { + // NOT thread-safe over a request because it modifies the + // global UmbracoContext.Current.InPreviewMode status. So it + // should never execute in // over the same UmbracoContext with + // different preview modes. + static string RenderRteMacros(string source, bool preview) + { + // save and set for macro rendering + var inPreviewMode = UmbracoContext.Current.InPreviewMode; + UmbracoContext.Current.InPreviewMode = preview; + + var sb = new StringBuilder(); + var umbracoHelper = new UmbracoHelper(UmbracoContext.Current); + MacroTagParser.ParseMacros( + source, + //callback for when text block is found + textBlock => sb.Append(textBlock), + //callback for when macro syntax is found + (macroAlias, macroAttributes) => sb.Append(umbracoHelper.RenderMacro( + macroAlias, + //needs to be explicitly casted to Dictionary + macroAttributes.ConvertTo(x => (string)x, x => x)).ToString())); + + // restore + UmbracoContext.Current.InPreviewMode = inPreviewMode; + + return sb.ToString(); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return null; + var sourceString = source.ToString(); + + // ensures string is parsed for {localLink} and urls are resolved correctly + sourceString = TemplateUtilities.ParseInternalLinks(sourceString, preview); + sourceString = TemplateUtilities.ResolveUrlsFromTextString(sourceString); + // ensure string is parsed for macros and macros are executed correctly + sourceString = RenderRteMacros(sourceString, preview); + + return sourceString; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index a4634e5331..944b85c576 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -24,6 +24,15 @@ namespace Umbraco.Web.PublishedCache UmbracoContext = umbracoContext; } + /// + /// Informs the contextual cache that content has changed. + /// + /// The contextual cache may, although that is not mandatory, provide an immutable snapshot of + /// the content over the duration of the context. If you make changes to the content and do want to have + /// the cache update its snapshot, you have to explicitely ask it to do so by calling ContentHasChanged. + public virtual void ContentHasChanged() + { } + /// /// Gets a content identified by its unique identifier. /// @@ -201,6 +210,12 @@ namespace Umbraco.Web.PublishedCache /// The XPath navigator. public abstract XPathNavigator GetXPathNavigator(bool preview); + /// + /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator + /// and that navigator is a NavigableNavigator. + /// + public abstract bool XPathNavigatorIsNavigable { get; } + /// /// Gets a value indicating whether the underlying non-contextual cache contains content. /// diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs index 9e221cbb1a..6e897c2c2e 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCacheOfT.cs @@ -12,6 +12,8 @@ namespace Umbraco.Web.PublishedCache /// Provides access to cached contents in a specified context. /// /// The type of the underlying published cache. + /// The type differenciates between the content cache and the media cache, + /// ie it will be either IPublishedContentCache or IPublishedMediaCache. public abstract class ContextualPublishedCache : ContextualPublishedCache where T : IPublishedCache { @@ -132,6 +134,12 @@ namespace Umbraco.Web.PublishedCache return _cache.GetXPathNavigator(UmbracoContext, preview); } + /// + /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator + /// and that navigator is a NavigableNavigator. + /// + public override bool XPathNavigatorIsNavigable { get { return _cache.XPathNavigatorIsNavigable; } } + /// /// Gets a value indicating whether the underlying non-contextual cache contains content. /// diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 16986f0148..a900689350 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -88,6 +88,12 @@ namespace Umbraco.Web.PublishedCache /// The value of overrides the context. XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview); + /// + /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator + /// and that navigator is a NavigableNavigator. + /// + bool XPathNavigatorIsNavigable { get; } + /// /// Gets a value indicating whether the cache contains published content. /// diff --git a/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs b/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs index 129b876a4f..d060ac554b 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCachesResolver.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.PublishedCache /// /// The caches. /// For developers, at application startup. - public void SetCache(IPublishedCaches caches) + public void SetCaches(IPublishedCaches caches) { Value = caches; } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 613abf8dfc..1d519744d9 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; using Umbraco.Web.Routing; using umbraco; @@ -237,14 +238,17 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache #region Converters - private static IPublishedContent ConvertToDocument(XmlNode xmlNode) + private static IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing) { - return xmlNode == null ? null : new Models.XmlPublishedContent(xmlNode); - } + return xmlNode == null + ? null + : PublishedContentModelFactory.CreateModel(new XmlPublishedContent(xmlNode, isPreviewing)); + } - private static IEnumerable ConvertToDocuments(XmlNodeList xmlNodes) + private static IEnumerable ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing) { - return xmlNodes.Cast().Select(xmlNode => new Models.XmlPublishedContent(xmlNode)); + return xmlNodes.Cast() + .Select(xmlNode => PublishedContentModelFactory.CreateModel(new XmlPublishedContent(xmlNode, isPreviewing))); } #endregion @@ -253,12 +257,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId) { - return ConvertToDocument(GetXml(umbracoContext, preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture))); + return ConvertToDocument(GetXml(umbracoContext, preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)), preview); } public virtual IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) { - return ConvertToDocuments(GetXml(umbracoContext, preview).SelectNodes(XPathStrings.RootDocuments)); + return ConvertToDocuments(GetXml(umbracoContext, preview).SelectNodes(XPathStrings.RootDocuments), preview); } public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, params XPathVariable[] vars) @@ -270,7 +274,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var node = vars == null ? xml.SelectSingleNode(xpath) : xml.SelectSingleNode(xpath, vars); - return ConvertToDocument(node); + return ConvertToDocument(node, preview); } public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, params XPathVariable[] vars) @@ -281,7 +285,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var node = vars == null ? xml.SelectSingleNode(xpath) : xml.SelectSingleNode(xpath, vars); - return ConvertToDocument(node); + return ConvertToDocument(node, preview); } public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, params XPathVariable[] vars) @@ -293,7 +297,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var nodes = vars == null ? xml.SelectNodes(xpath) : xml.SelectNodes(xpath, vars); - return ConvertToDocuments(nodes); + return ConvertToDocuments(nodes, preview); } public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, params XPathVariable[] vars) @@ -304,7 +308,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache var nodes = vars == null ? xml.SelectNodes(xpath) : xml.SelectNodes(xpath, vars); - return ConvertToDocuments(nodes); + return ConvertToDocuments(nodes, preview); } public virtual bool HasContent(UmbracoContext umbracoContext, bool preview) @@ -322,6 +326,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return xml.CreateNavigator(); } + public virtual bool XPathNavigatorIsNavigable { get { return false; } } + #endregion #region Legacy Xml diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index fff4528a2c..dc00728bfc 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Dynamics; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; using Umbraco.Web.Models; using UmbracoExamine; @@ -28,7 +29,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// /// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly. /// - internal class PublishedMediaCache : IPublishedMediaCache + internal class PublishedMediaCache : IPublishedMediaCache { public PublishedMediaCache() { @@ -93,6 +94,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache throw new NotImplementedException("PublishedMediaCache does not support XPath."); } + public bool XPathNavigatorIsNavigable { get { return false; } } + public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); } private ExamineManager GetExamineManagerSafe() @@ -238,7 +241,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } - return new DictionaryPublishedContent(values, + var content = new DictionaryPublishedContent(values, d => d.ParentId != -1 //parent should be null if -1 ? GetUmbracoMedia(d.ParentId) : null, @@ -246,6 +249,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache d => GetChildrenMedia(d.Id), GetProperty, true); + return PublishedContentModelFactory.CreateModel(content); } internal IPublishedContent ConvertFromXPathNavigator(XPathNavigator xpath) @@ -296,7 +300,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - return new DictionaryPublishedContent(values, + var content = new DictionaryPublishedContent(values, d => d.ParentId != -1 //parent should be null if -1 ? GetUmbracoMedia(d.ParentId) : null, @@ -304,6 +308,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache d => GetChildrenMedia(d.Id, xpath), GetProperty, false); + return PublishedContentModelFactory.CreateModel(content); } /// @@ -313,19 +318,23 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// /// /// - private IPublishedContentProperty GetProperty(DictionaryPublishedContent dd, string alias) + private IPublishedProperty GetProperty(DictionaryPublishedContent dd, string alias) { if (dd.LoadedFromExamine) { //if this is from Examine, lets check if the alias does not exist on the document - if (dd.Properties.All(x => x.Alias != alias)) + if (dd.Properties.All(x => x.PropertyTypeAlias != alias)) { //ok it doesn't exist, we might assume now that Examine didn't index this property because the index is not set up correctly //so before we go loading this from the database, we can check if the alias exists on the content type at all, this information //is cached so will be quicker to look up. - if (dd.Properties.Any(x => x.Alias == UmbracoContentIndexer.NodeTypeAliasFieldName)) + if (dd.Properties.Any(x => x.PropertyTypeAlias == UmbracoContentIndexer.NodeTypeAliasFieldName)) { - var aliasesAndNames = ContentType.GetAliasesAndNames(dd.Properties.First(x => x.Alias.InvariantEquals(UmbracoContentIndexer.NodeTypeAliasFieldName)).Value.ToString()); + // so in dd.Properties, there is an IPublishedProperty with property type alias "__NodeTypeAlias" and + // that special property would contain the node type alias, which we use to get "aliases & names". That + // special property is going to be a PropertyResult (with ObjectValue == DataValue) and we + // want its value in the most simple way = it is OK to use DataValue here. + var aliasesAndNames = ContentType.GetAliasesAndNames(dd.Properties.First(x => x.PropertyTypeAlias.InvariantEquals(UmbracoContentIndexer.NodeTypeAliasFieldName)).DataValue.ToString()); if (aliasesAndNames != null) { if (!aliasesAndNames.ContainsKey(alias)) @@ -342,7 +351,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { media.MoveNext(); var mediaDoc = ConvertFromXPathNavigator(media.Current); - return mediaDoc.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + return mediaDoc.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); } } } @@ -350,9 +359,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //We've made it here which means that the value is stored in the Examine index. //We are going to check for a special field however, that is because in some cases we store a 'Raw' //value in the index such as for xml/html. - var rawValue = dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(UmbracoContentIndexer.RawFieldPrefix + alias)); + var rawValue = dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(UmbracoContentIndexer.RawFieldPrefix + alias)); return rawValue - ?? dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + ?? dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); } /// @@ -465,12 +474,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// internal class DictionaryPublishedContent : PublishedContentBase { + // note: I'm not sure this class fully complies with IPublishedContent rules especially + // I'm not sure that _properties contains all properties including those without a value, + // neither that GetProperty will return a property without a value vs. null... @zpqrtbnk public DictionaryPublishedContent( IDictionary valueDictionary, Func getParent, Func> getChildren, - Func getProperty, + Func getProperty, bool fromExamine) { if (valueDictionary == null) throw new ArgumentNullException("valueDictionary"); @@ -508,15 +520,27 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } }, "parentID"); - _properties = new Collection(); + _contentType = PublishedContentType.Get(PublishedItemType.Media, _documentTypeAlias); + _properties = new Collection(); //loop through remaining values that haven't been applied foreach (var i in valueDictionary.Where(x => !_keysAdded.Contains(x.Key))) { - //this is taken from examine - _properties.Add(i.Key.InvariantStartsWith("__") - ? new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty) - : new PropertyResult(i.Key, i.Value, PropertyResultType.UserProperty)); + IPublishedProperty property; + + if (i.Key.InvariantStartsWith("__")) + { + // no type for that one, dunno how to convert + property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty); + } + else + { + // use property type to ensure proper conversion + var propertyType = _contentType.GetPropertyType(i.Key); + property = new XmlPublishedProperty(propertyType, false, i.Value); // false :: never preview a media + } + + _properties.Add(property); } } @@ -545,7 +569,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private readonly Func _getParent; private readonly Func> _getChildren; - private readonly Func _getProperty; + private readonly Func _getProperty; /// /// Returns 'Media' as the item type @@ -645,7 +669,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache get { return _level; } } - public override ICollection Properties + public override bool IsDraft + { + get { return false; } + } + + public override ICollection Properties { get { return _properties; } } @@ -655,11 +684,49 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache get { return _getChildren(this); } } - public override IPublishedContentProperty GetProperty(string alias) + public override IPublishedProperty GetProperty(string alias) { return _getProperty(this, alias); } + public override PublishedContentType ContentType + { + get { return _contentType; } + } + + // override to implement cache + // cache at context level, ie once for the whole request + // but cache is not shared by requests because we wouldn't know how to clear it + public override IPublishedProperty GetProperty(string alias, bool recurse) + { + if (recurse == false) return GetProperty(alias); + + IPublishedProperty property; + string key = null; + var cache = UmbracoContextCache.Current; + + if (cache != null) + { + key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); + object o; + if (cache.TryGetValue(key, out o)) + { + property = o as IPublishedProperty; + if (property == null) + throw new InvalidOperationException("Corrupted cache."); + return property; + } + } + + // else get it for real, no cache + property = base.GetProperty(alias, true); + + if (cache != null) + cache[key] = property; + + return property; + } + private readonly List _keysAdded = new List(); private int _id; private int _templateId; @@ -677,7 +744,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private DateTime _updateDate; private Guid _version; private int _level; - private readonly ICollection _properties; + private readonly ICollection _properties; + private readonly PublishedContentType _contentType; private void ValidateAndSetProperty(IDictionary valueDictionary, Action setProperty, params string[] potentialKeys) { @@ -690,6 +758,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache setProperty(valueDictionary[key]); _keysAdded.Add(key); } - } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs index 1775d37fe0..9c8eed322b 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/RoutesCache.cs @@ -48,7 +48,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache global::umbraco.content.AfterUpdateDocumentCache += (sender, e) => Clear(); global::umbraco.content.AfterClearDocumentCache += (sender, e) => Clear(); - // fixme - refactor when content events are refactored + // fixme - should refactor once content events are refactored // the content class needs to be refactored - at the moment // content.XmlContentInternal setter does not trigger any event // content.UpdateDocumentCache(List Documents) does not trigger any event diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/UmbracoContextCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/UmbracoContextCache.cs new file mode 100644 index 0000000000..6b82a7ee3d --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/UmbracoContextCache.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Runtime.CompilerServices; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + static class UmbracoContextCache + { + static readonly ConditionalWeakTable> Caches + = new ConditionalWeakTable>(); + + public static ConcurrentDictionary Current + { + get + { + var umbracoContext = UmbracoContext.Current; + + // will get or create a value + // a ConditionalWeakTable is thread-safe + // does not prevent the context from being disposed, and then the dictionary will be disposed too + return umbracoContext == null ? null : Caches.GetOrCreateValue(umbracoContext); + } + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs new file mode 100644 index 0000000000..5bcca93da5 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Xml; +using System.Xml.Serialization; +using System.Xml.XPath; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + + /// + /// Represents an IPublishedContent which is created based on an Xml structure. + /// + [Serializable] + [XmlType(Namespace = "http://umbraco.org/webservices/")] + internal class XmlPublishedContent : PublishedContentBase + { + /// + /// Initializes a new instance of the XmlPublishedContent class with an Xml node. + /// + /// The Xml node. + /// A value indicating whether the published content is being previewed. + public XmlPublishedContent(XmlNode xmlNode, bool isPreviewing) + { + _xmlNode = xmlNode; + _isPreviewing = isPreviewing; + InitializeStructure(); + Initialize(); + InitializeChildren(); + } + + /// + /// Initializes a new instance of the XmlPublishedContent class with an Xml node, + /// and a value indicating whether to lazy-initialize the instance. + /// + /// The Xml node. + /// A value indicating whether the published content is being previewed. + /// A value indicating whether to lazy-initialize the instance. + /// Lazy-initializationg is NOT thread-safe. + internal XmlPublishedContent(XmlNode xmlNode, bool isPreviewing, bool lazyInitialize) + { + _xmlNode = xmlNode; + _isPreviewing = isPreviewing; + InitializeStructure(); + if (lazyInitialize == false) + { + Initialize(); + InitializeChildren(); + } + } + + private readonly XmlNode _xmlNode; + + private bool _initialized; + private bool _childrenInitialized; + + private readonly ICollection _children = new Collection(); + private IPublishedContent _parent; + + private int _id; + private int _template; + private string _name; + private string _docTypeAlias; + private int _docTypeId; + private string _writerName; + private string _creatorName; + private int _writerId; + private int _creatorId; + private string _urlName; + private string _path; + private DateTime _createDate; + private DateTime _updateDate; + private Guid _version; + private IPublishedProperty[] _properties; + private int _sortOrder; + private int _level; + private bool _isDraft; + private readonly bool _isPreviewing; + private PublishedContentType _contentType; + + public override IEnumerable Children + { + get + { + if (_initialized == false) + Initialize(); + if (_childrenInitialized == false) + InitializeChildren(); + return _children.OrderBy(x => x.SortOrder); + } + } + + public override IPublishedProperty GetProperty(string alias) + { + return Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + // override to implement cache + // cache at context level, ie once for the whole request + // but cache is not shared by requests because we wouldn't know how to clear it + public override IPublishedProperty GetProperty(string alias, bool recurse) + { + if (recurse == false) return GetProperty(alias); + + var cache = UmbracoContextCache.Current; + + if (cache == null) + return base.GetProperty(alias, true); + + var key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); + var value = cache.GetOrAdd(key, k => base.GetProperty(alias, true)); + if (value == null) + return null; + + var property = value as IPublishedProperty; + if (property == null) + throw new InvalidOperationException("Corrupted cache."); + + return property; + } + + public override PublishedItemType ItemType + { + get { return PublishedItemType.Content; } + } + + public override IPublishedContent Parent + { + get + { + if (_initialized == false) + Initialize(); + return _parent; + } + } + + public override int Id + { + get + { + if (_initialized == false) + Initialize(); + return _id; + } + } + + public override int TemplateId + { + get + { + if (_initialized == false) + Initialize(); + return _template; + } + } + + public override int SortOrder + { + get + { + if (_initialized == false) + Initialize(); + return _sortOrder; + } + } + + public override string Name + { + get + { + if (_initialized == false) + Initialize(); + return _name; + } + } + + public override string DocumentTypeAlias + { + get + { + if (_initialized == false) + Initialize(); + return _docTypeAlias; + } + } + + public override int DocumentTypeId + { + get + { + if (_initialized == false) + Initialize(); + return _docTypeId; + } + } + + public override string WriterName + { + get + { + if (_initialized == false) + Initialize(); + return _writerName; + } + } + + public override string CreatorName + { + get + { + if (_initialized == false) + Initialize(); + return _creatorName; + } + } + + public override int WriterId + { + get + { + if (_initialized == false) + Initialize(); + return _writerId; + } + } + + public override int CreatorId + { + get + { + if (_initialized == false) + Initialize(); + return _creatorId; + } + } + + public override string Path + { + get + { + if (_initialized == false) + Initialize(); + return _path; + } + } + + public override DateTime CreateDate + { + get + { + if (_initialized == false) + Initialize(); + return _createDate; + } + } + + public override DateTime UpdateDate + { + get + { + if (_initialized == false) + Initialize(); + return _updateDate; + } + } + + public override Guid Version + { + get + { + if (_initialized == false) + Initialize(); + return _version; + } + } + + public override string UrlName + { + get + { + if (_initialized == false) + Initialize(); + return _urlName; + } + } + + public override int Level + { + get + { + if (_initialized == false) + Initialize(); + return _level; + } + } + + public override bool IsDraft + { + get + { + if (_initialized == false) + Initialize(); + return _isDraft; + } + } + + public override ICollection Properties + { + get + { + if (_initialized == false) + Initialize(); + return _properties; + } + } + + public override PublishedContentType ContentType + { + get + { + if (_initialized == false) + Initialize(); + return _contentType; + } + } + + private void InitializeStructure() + { + // load parent if it exists and is a node + + var parent = _xmlNode == null ? null : _xmlNode.ParentNode; + if (parent == null) return; + + if (parent.Name == "node" || (parent.Attributes != null && parent.Attributes.GetNamedItem("isDoc") != null)) + _parent = PublishedContentModelFactory.CreateModel(new XmlPublishedContent(parent, _isPreviewing, true)); + } + + private void Initialize() + { + if (_xmlNode == null) return; + + if (_xmlNode.Attributes != null) + { + _id = int.Parse(_xmlNode.Attributes.GetNamedItem("id").Value); + if (_xmlNode.Attributes.GetNamedItem("template") != null) + _template = int.Parse(_xmlNode.Attributes.GetNamedItem("template").Value); + if (_xmlNode.Attributes.GetNamedItem("sortOrder") != null) + _sortOrder = int.Parse(_xmlNode.Attributes.GetNamedItem("sortOrder").Value); + if (_xmlNode.Attributes.GetNamedItem("nodeName") != null) + _name = _xmlNode.Attributes.GetNamedItem("nodeName").Value; + if (_xmlNode.Attributes.GetNamedItem("writerName") != null) + _writerName = _xmlNode.Attributes.GetNamedItem("writerName").Value; + if (_xmlNode.Attributes.GetNamedItem("urlName") != null) + _urlName = _xmlNode.Attributes.GetNamedItem("urlName").Value; + // Creatorname is new in 2.1, so published xml might not have it! + try + { + _creatorName = _xmlNode.Attributes.GetNamedItem("creatorName").Value; + } + catch + { + _creatorName = _writerName; + } + + //Added the actual userID, as a user cannot be looked up via full name only... + if (_xmlNode.Attributes.GetNamedItem("creatorID") != null) + _creatorId = int.Parse(_xmlNode.Attributes.GetNamedItem("creatorID").Value); + if (_xmlNode.Attributes.GetNamedItem("writerID") != null) + _writerId = int.Parse(_xmlNode.Attributes.GetNamedItem("writerID").Value); + + if (UmbracoSettings.UseLegacyXmlSchema) + { + if (_xmlNode.Attributes.GetNamedItem("nodeTypeAlias") != null) + _docTypeAlias = _xmlNode.Attributes.GetNamedItem("nodeTypeAlias").Value; + } + else + { + _docTypeAlias = _xmlNode.Name; + } + + if (_xmlNode.Attributes.GetNamedItem("nodeType") != null) + _docTypeId = int.Parse(_xmlNode.Attributes.GetNamedItem("nodeType").Value); + if (_xmlNode.Attributes.GetNamedItem("path") != null) + _path = _xmlNode.Attributes.GetNamedItem("path").Value; + if (_xmlNode.Attributes.GetNamedItem("version") != null) + _version = new Guid(_xmlNode.Attributes.GetNamedItem("version").Value); + if (_xmlNode.Attributes.GetNamedItem("createDate") != null) + _createDate = DateTime.Parse(_xmlNode.Attributes.GetNamedItem("createDate").Value); + if (_xmlNode.Attributes.GetNamedItem("updateDate") != null) + _updateDate = DateTime.Parse(_xmlNode.Attributes.GetNamedItem("updateDate").Value); + if (_xmlNode.Attributes.GetNamedItem("level") != null) + _level = int.Parse(_xmlNode.Attributes.GetNamedItem("level").Value); + + _isDraft = (_xmlNode.Attributes.GetNamedItem("isDraft") != null); + } + + // load data + var dataXPath = UmbracoSettings.UseLegacyXmlSchema ? "data" : "* [not(@isDoc)]"; + var nodes = _xmlNode.SelectNodes(dataXPath); + + _contentType = PublishedContentType.Get(PublishedItemType.Content, _docTypeAlias); + + var propertyNodes = new Dictionary(); + if (nodes != null) + foreach (XmlNode n in nodes) + { + var alias = UmbracoSettings.UseLegacyXmlSchema + ? n.Attributes.GetNamedItem("alias").Value + : n.Name; + propertyNodes[alias.ToLowerInvariant()] = n; + } + + _properties = _contentType.PropertyTypes.Select(p => + { + XmlNode n; + return propertyNodes.TryGetValue(p.PropertyTypeAlias.ToLowerInvariant(), out n) + ? new XmlPublishedProperty(p, _isPreviewing, n) + : new XmlPublishedProperty(p, _isPreviewing); + }).Cast().ToArray(); + + // warn: this is not thread-safe... + _initialized = true; + } + + private void InitializeChildren() + { + if (_xmlNode == null) return; + + // load children + var childXPath = UmbracoSettings.UseLegacyXmlSchema ? "node" : "* [@isDoc]"; + var nav = _xmlNode.CreateNavigator(); + var expr = nav.Compile(childXPath); + expr.AddSort("@sortOrder", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number); + var iterator = nav.Select(expr); + while (iterator.MoveNext()) + _children.Add(PublishedContentModelFactory.CreateModel( + new XmlPublishedContent(((IHasXmlNode)iterator.Current).GetNode(), _isPreviewing, true))); + + // warn: this is not thread-safe + _childrenInitialized = true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs new file mode 100644 index 0000000000..a75ea7c4a3 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedProperty.cs @@ -0,0 +1,67 @@ +using System; +using System.Xml; +using System.Xml.Serialization; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + + /// + /// Represents an IDocumentProperty which is created based on an Xml structure. + /// + [Serializable] + [XmlType(Namespace = "http://umbraco.org/webservices/")] + internal class XmlPublishedProperty : PublishedPropertyBase + { + private readonly string _xmlValue; // the raw, xml node value + private readonly Lazy _sourceValue; + private readonly Lazy _objectValue; + private readonly Lazy _xpathValue; + private readonly bool _isPreviewing; + + /// + /// Gets the raw value of the property. + /// + public override object DataValue { get { return _xmlValue; } } + + // in the Xml cache, everything is a string, and to have a value + // you want to have a non-null, non-empty string. + public override bool HasValue + { + get { return _xmlValue.Trim().Length > 0; } + } + + public override object ObjectValue { get { return _objectValue.Value; } } + public override object XPathValue { get { return _xpathValue.Value; } } + + public XmlPublishedProperty(PublishedPropertyType propertyType, bool isPreviewing, XmlNode propertyXmlData) + : this(propertyType, isPreviewing) + { + if (propertyXmlData == null) + throw new ArgumentNullException("propertyXmlData", "Property xml source is null"); + _xmlValue = XmlHelper.GetNodeValue(propertyXmlData); + } + + public XmlPublishedProperty(PublishedPropertyType propertyType, bool isPreviewing, string propertyData) + : this(propertyType, isPreviewing) + { + if (propertyData == null) + throw new ArgumentNullException("propertyData"); + _xmlValue = propertyData; + } + + public XmlPublishedProperty(PublishedPropertyType propertyType, bool isPreviewing) + : base(propertyType) + { + _xmlValue = string.Empty; + _isPreviewing = isPreviewing; + + _sourceValue = new Lazy(() => PropertyType.ConvertDataToSource(_xmlValue, _isPreviewing)); + _objectValue = new Lazy(() => PropertyType.ConvertSourceToObject(_sourceValue.Value, _isPreviewing)); + _xpathValue = new Lazy(() => PropertyType.ConvertSourceToXPath(_sourceValue.Value, _isPreviewing)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 574a1dc332..65038f01c6 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,275 +1,433 @@ +// fixme - should #define - but when will it be OK? +// axes navigation is broken in many ways... but fixes would not be 100% +// backward compatible... so keep them for v7 or whenever appropriate. +#undef FIX_AXES + using System; -using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Data; using System.Linq; using System.Web; using Examine.LuceneEngine.SearchCriteria; -using Umbraco.Core.Dynamics; using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Models; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Templates; -using umbraco; -using umbraco.cms.businesslogic; using Umbraco.Core; -using umbraco.cms.businesslogic.template; -using umbraco.interfaces; using ContentType = umbraco.cms.businesslogic.ContentType; -using Template = umbraco.cms.businesslogic.template.Template; namespace Umbraco.Web { - /// - /// Extension methods for IPublishedContent - /// - /// - /// These methods exist in the web project as we need access to web based classes like NiceUrl provider - /// which is why they cannot exist in the Core project. - /// + /// + /// Provides extension methods for IPublishedContent. + /// public static class PublishedContentExtensions - { + { + #region Urls - /// - /// Converts an INode to an IPublishedContent item + /// + /// Gets the url for the content. /// - /// - /// - internal static IPublishedContent ConvertFromNode(this INode node) - { - var umbHelper = new UmbracoHelper(UmbracoContext.Current); - return umbHelper.TypedContent(node.Id); - } - - /// - /// Gets the NiceUrl for the content item - /// - /// - /// + /// The content. + /// The url for the content. [Obsolete("NiceUrl() is obsolete, use the Url() method instead")] - public static string NiceUrl(this IPublishedContent doc) + public static string NiceUrl(this IPublishedContent content) { - return doc.Url(); + return content.Url(); } /// - /// Gets the Url for the content item + /// Gets the url for the content. /// - /// - /// - public static string Url(this IPublishedContent doc) + /// The content. + /// The url for the content. + /// Better use the Url property but that method is here to complement UrlAbsolute(). + public static string Url(this IPublishedContent content) { - switch (doc.ItemType) - { - case PublishedItemType.Content: - var umbHelper = new UmbracoHelper(UmbracoContext.Current); - return umbHelper.NiceUrl(doc.Id); - case PublishedItemType.Media: - var prop = doc.GetProperty(Constants.Conventions.Media.File); - if (prop == null) - throw new NotSupportedException("Cannot retreive a Url for a media item if there is no 'umbracoFile' property defined"); - return prop.Value.ToString(); - default: - throw new ArgumentOutOfRangeException(); - } + return content.Url; } /// - /// Gets the NiceUrlWithDomain for the content item + /// Gets the absolute url for the content. /// - /// - /// - [Obsolete("NiceUrlWithDomain() is obsolete, use the UrlWithDomain() method instead")] - public static string NiceUrlWithDomain(this IPublishedContent doc) + /// The content. + /// The absolute url for the content. + [Obsolete("NiceUrlWithDomain() is obsolete, use the UrlAbsolute() method instead.")] + public static string NiceUrlWithDomain(this IPublishedContent content) { - return doc.UrlWithDomain(); + return content.UrlAbsolute(); } /// - /// Gets the UrlWithDomain for the content item + /// Gets the absolute url for the content. /// - /// - /// - public static string UrlWithDomain(this IPublishedContent doc) + /// The content. + /// The absolute url for the content. + //[Obsolete("UrlWithDomain() is obsolete, use the UrlAbsolute() method instead.")] + public static string UrlWithDomain(this IPublishedContent content) { - switch (doc.ItemType) - { - case PublishedItemType.Content: - var umbHelper = new UmbracoHelper(UmbracoContext.Current); - return umbHelper.NiceUrlWithDomain(doc.Id); - case PublishedItemType.Media: - throw new NotSupportedException("NiceUrlWithDomain is not supported for media types"); - default: - throw new ArgumentOutOfRangeException(); - } + return content.UrlAbsolute(); } - /// + /// + /// Gets the absolute url for the content. + /// + /// The content. + /// The absolute url for the content. + public static string UrlAbsolute(this IPublishedContent content) + { + // adapted from PublishedContentBase.Url + switch (content.ItemType) + { + case PublishedItemType.Content: + if (UmbracoContext.Current == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current is null."); + if (UmbracoContext.Current.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); + return UmbracoContext.Current.UrlProvider.GetUrl(content.Id); + case PublishedItemType.Media: + throw new NotSupportedException("AbsoluteUrl is not supported for media types."); + default: + throw new ArgumentOutOfRangeException(); + } + } + + #endregion + + #region Template + + /// /// Returns the current template Alias /// - /// + /// /// - public static string GetTemplateAlias(this IPublishedContent doc) - { - var template = Template.GetTemplate(doc.TemplateId); - return template != null ? template.Alias : string.Empty; + public static string GetTemplateAlias(this IPublishedContent content) + { + var template = ApplicationContext.Current.Services.FileService.GetTemplate(content.TemplateId); + return template == null ? string.Empty : template.Alias; } - #region GetPropertyValue + #endregion - /// - /// if the val is a string, ensures all internal local links are parsed + #region HasProperty + + /// + /// Gets a value indicating whether the content has a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether the content has the property identified by the alias. + /// The content may have a property, and that property may not have a value. + public static bool HasProperty(this IPublishedContent content, string alias) + { + return content.ContentType.GetPropertyType(alias) != null; + } + + #endregion + + #region HasValue + + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if GetProperty(alias) is not null and GetProperty(alias).HasValue is true. + public static bool HasValue(this IPublishedContent content, string alias) + { + return content.HasValue(alias, false); + } + + /// + /// Gets a value indicating whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether to navigate the tree upwards until a property with a value is found. + /// A value indicating whether the content has a value for the property identified by the alias. + /// Returns true if GetProperty(alias, recurse) is not null and GetProperty(alias, recurse).HasValue is true. + public static bool HasValue(this IPublishedContent content, string alias, bool recurse) + { + var prop = content.GetProperty(alias, recurse); + return prop != null && prop.HasValue; + } + + /// + /// Returns one of two strings depending on whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// The value to return if the content has a value for the property. + /// The value to return if the content has no value for the property. + /// Either or depending on whether the content + /// has a value for the property identified by the alias. + public static IHtmlString HasValue(this IPublishedContent content, string alias, + string valueIfTrue, string valueIfFalse = null) + { + return content.HasValue(alias, false) + ? new HtmlString(valueIfTrue) + : new HtmlString(valueIfFalse ?? string.Empty); + } + + /// + /// Returns one of two strings depending on whether the content has a value for a property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether to navigate the tree upwards until a property with a value is found. + /// The value to return if the content has a value for the property. + /// The value to return if the content has no value for the property. + /// Either or depending on whether the content + /// has a value for the property identified by the alias. + public static IHtmlString HasValue(this IPublishedContent content, string alias, bool recurse, + string valueIfTrue, string valueIfFalse = null) + { + return content.HasValue(alias, recurse) + ? new HtmlString(valueIfTrue) + : new HtmlString(valueIfFalse ?? string.Empty); + } + + #endregion + + #region GetPropertyValue + + /// + /// Gets the value of a content's property identified by its alias. + /// + /// The content. + /// The property alias. + /// The value of the content's property identified by the alias. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns null. + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedContent content, string alias) + { + var property = content.GetProperty(alias); + return property == null ? null : property.ObjectValue; + } + + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedContent content, string alias, string defaultValue) + { + var property = content.GetProperty(alias); + return property == null || property.HasValue == false ? defaultValue : property.ObjectValue; + } + + /// + /// Gets the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedContent content, string alias, object defaultValue) + { + var property = content.GetProperty(alias); + return property == null || property.HasValue == false ? defaultValue : property.ObjectValue; + } + + /// + /// Recursively gets the value of a content's property identified by its alias. + /// + /// The content. + /// The property alias. + /// A value indicating whether to recurse. + /// The recursive value of the content's property identified by the alias. + /// + /// Recursively means: walking up the tree from , get the first value that can be found. + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns null. + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedContent content, string alias, bool recurse) + { + var property = content.GetProperty(alias, recurse); + return property == null ? null : property.ObjectValue; + } + + /// + /// Recursively the value of a content's property identified by its alias, if it exists, otherwise a default value. + /// + /// The content. + /// The property alias. + /// A value indicating whether to recurse. + /// The default value. + /// The value of the content's property identified by the alias, if it exists, otherwise a default value. + /// + /// Recursively means: walking up the tree from , get the first value that can be found. + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static object GetPropertyValue(this IPublishedContent content, string alias, bool recurse, object defaultValue) + { + var property = content.GetProperty(alias, recurse); + return property == null || property.HasValue == false ? defaultValue : property.ObjectValue; + } + + #endregion + + #region GetPropertyValue + + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type. /// - /// - /// - internal static object GetValueWithParsedLinks(object val) + /// The target property type. + /// The content. + /// The property alias. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T GetPropertyValue(this IPublishedContent content, string alias) { - //if it is a string send it through the url parser - var text = val as string; - if (text != null) - { - return TemplateUtilities.ResolveUrlsFromTextString( - TemplateUtilities.ParseInternalLinks(text)); - } - //its not a string - return val; + return content.GetPropertyValue(alias, false, false, default(T)); } - public static object GetPropertyValue(this IPublishedContent doc, string alias) - { - return doc.GetPropertyValue(alias, false); - } - public static object GetPropertyValue(this IPublishedContent doc, string alias, string fallback) - { - var prop = doc.GetPropertyValue(alias); - return (prop != null && !Convert.ToString(prop).IsNullOrWhiteSpace()) ? prop : fallback; - } - public static object GetPropertyValue(this IPublishedContent doc, string alias, bool recursive) - { - var p = doc.GetProperty(alias, recursive); - if (p == null) return null; + /// + /// Gets the value of a content's property identified by its alias, converted to a specified type, if it exists, otherwise a default value. + /// + /// The target property type. + /// The content. + /// The property alias. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. + /// + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T GetPropertyValue(this IPublishedContent content, string alias, T defaultValue) + { + return content.GetPropertyValue(alias, false, true, defaultValue); + } - //Here we need to put the value through the IPropertyEditorValueConverter's - //get the data type id for the current property - var propertyEditor = PublishedContentHelper.GetPropertyEditor( - ApplicationContext.Current, doc.DocumentTypeAlias, alias, - doc.ItemType); + /// + /// Recursively gets the value of a content's property identified by its alias, converted to a specified type. + /// + /// The target property type. + /// The content. + /// The property alias. + /// A value indicating whether to recurse. + /// The value of the content's property identified by the alias, converted to the specified type. + /// + /// Recursively means: walking up the tree from , get the first value that can be found. + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns default(T). + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T GetPropertyValue(this IPublishedContent content, string alias, bool recurse) + { + return content.GetPropertyValue(alias, recurse, false, default(T)); + } - var def = new PublishedPropertyDefinition(alias, doc.DocumentTypeAlias, propertyEditor); + /// + /// Recursively gets the value of a content's property identified by its alias, converted to a specified type, if it exists, otherwise a default value. + /// + /// The target property type. + /// The content. + /// The property alias. + /// A value indicating whether to recurse. + /// The default value. + /// The value of the content's property identified by the alias, converted to the specified type, if it exists, otherwise a default value. + /// + /// Recursively means: walking up the tree from , get the first value that can be found. + /// The value comes from IPublishedProperty field Value ie it is suitable for use when rendering content. + /// If no property with the specified alias exists, or if the property has no value, or if it could not be converted, returns . + /// If eg a numeric property wants to default to 0 when value source is empty, this has to be done in the converter. + /// The alias is case-insensitive. + /// + public static T GetPropertyValue(this IPublishedContent content, string alias, bool recurse, T defaultValue) + { + return content.GetPropertyValue(alias, recurse, true, defaultValue); + } - //convert the string value to a known type - var converted = PublishedContentHelper.ConvertPropertyValue(p.Value, def); - return converted.Success - ? GetValueWithParsedLinks(converted.Result) - : GetValueWithParsedLinks(p.Value); - } - public static object GetPropertyValue(this IPublishedContent doc, string alias, bool recursive, string fallback) - { - var prop = doc.GetPropertyValue(alias, recursive); - return (prop != null && !Convert.ToString(prop).IsNullOrWhiteSpace()) ? prop : fallback; - } + internal static T GetPropertyValue(this IPublishedContent content, string alias, bool recurse, bool withDefaultValue, T defaultValue) + { + var property = content.GetProperty(alias, recurse); + if (property == null) return defaultValue; - /// - /// Returns the property as the specified type, if the property is not found or does not convert - /// then the default value of type T is returned. - /// - /// - /// - /// - /// - public static T GetPropertyValue(this IPublishedContent doc, string alias) - { - return doc.GetPropertyValue(alias, default(T)); - } - - public static T GetPropertyValue(this IPublishedContent prop, string alias, bool recursive, T ifCannotConvert) - { - var p = prop.GetProperty(alias, recursive); - if (p == null) - return ifCannotConvert; - - //before we try to convert it manually, lets see if the PropertyEditorValueConverter does this for us - //Here we need to put the value through the IPropertyEditorValueConverter's - //get the property editor alias for the current property - var propertyEditor = PublishedContentHelper.GetPropertyEditor(ApplicationContext.Current, prop.DocumentTypeAlias, alias, prop.ItemType); - //convert the value to a known type - var def = new PublishedPropertyDefinition(alias, prop.DocumentTypeAlias, propertyEditor); - var converted = PublishedContentHelper.ConvertPropertyValue(p.Value, def); - object parsedLinksVal; - if (converted.Success) - { - parsedLinksVal = GetValueWithParsedLinks(converted.Result); - - //if its successful, check if its the correct type and return it - if (parsedLinksVal is T) - { - return (T)parsedLinksVal; - } - //if that's not correct, try converting the converted type - var reConverted = converted.Result.TryConvertTo(); - if (reConverted.Success) - { - return reConverted.Result; - } - } - - //first, parse links if possible - parsedLinksVal = GetValueWithParsedLinks(p.Value); - //last, if all the above has failed, we'll just try converting the raw value straight to 'T' - var manualConverted = parsedLinksVal.TryConvertTo(); - if (manualConverted.Success) - return manualConverted.Result; - return ifCannotConvert; - } - - public static T GetPropertyValue(this IPublishedContent prop, string alias, T ifCannotConvert) - { - return prop.GetPropertyValue(alias, false, ifCannotConvert); + return property.GetValue(withDefaultValue, defaultValue); } #endregion + // copied over from Core.PublishedContentExtensions - should be obsoleted + [Obsolete("GetRecursiveValue() is obsolete, use GetPropertyValue().")] + public static string GetRecursiveValue(this IPublishedContent content, string alias) + { + var value = content.GetPropertyValue(alias, true); + return value == null ? string.Empty : value.ToString(); + } + #region Search - public static IEnumerable Search(this IPublishedContent d, string term, bool useWildCards = true, string searchProvider = null) + + public static IEnumerable Search(this IPublishedContent content, string term, bool useWildCards = true, string searchProvider = null) { var searcher = Examine.ExamineManager.Instance.DefaultSearchProvider; - if (!string.IsNullOrEmpty(searchProvider)) + if (string.IsNullOrEmpty(searchProvider) == false) searcher = Examine.ExamineManager.Instance.SearchProviderCollection[searchProvider]; var t = term.Escape().Value; if (useWildCards) t = term.MultipleCharacterWildcard().Value; - string luceneQuery = "+__Path:(" + d.Path.Replace("-", "\\-") + "*) +" + t; + var luceneQuery = "+__Path:(" + content.Path.Replace("-", "\\-") + "*) +" + t; var crit = searcher.CreateSearchCriteria().RawQuery(luceneQuery); - return d.Search(crit, searcher); + return content.Search(crit, searcher); } - public static IEnumerable SearchDescendants(this IPublishedContent d, string term, bool useWildCards = true, string searchProvider = null) + public static IEnumerable SearchDescendants(this IPublishedContent content, string term, bool useWildCards = true, string searchProvider = null) { - return d.Search(term, useWildCards, searchProvider); + return content.Search(term, useWildCards, searchProvider); } - public static IEnumerable SearchChildren(this IPublishedContent d, string term, bool useWildCards = true, string searchProvider = null) + public static IEnumerable SearchChildren(this IPublishedContent content, string term, bool useWildCards = true, string searchProvider = null) { var searcher = Examine.ExamineManager.Instance.DefaultSearchProvider; - if (!string.IsNullOrEmpty(searchProvider)) + if (string.IsNullOrEmpty(searchProvider) == false) searcher = Examine.ExamineManager.Instance.SearchProviderCollection[searchProvider]; var t = term.Escape().Value; if (useWildCards) t = term.MultipleCharacterWildcard().Value; - string luceneQuery = "+parentID:" + d.Id.ToString() + " +" + t; + var luceneQuery = "+parentID:" + content.Id + " +" + t; var crit = searcher.CreateSearchCriteria().RawQuery(luceneQuery); - return d.Search(crit, searcher); + return content.Search(crit, searcher); } - public static IEnumerable Search(this IPublishedContent d, Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) + public static IEnumerable Search(this IPublishedContent content, Examine.SearchCriteria.ISearchCriteria criteria, Examine.Providers.BaseSearchProvider searchProvider = null) { var s = Examine.ExamineManager.Instance.DefaultSearchProvider; if (searchProvider != null) @@ -278,153 +436,54 @@ namespace Umbraco.Web var results = s.Search(criteria); return results.ConvertSearchResultToPublishedContent(UmbracoContext.Current.ContentCache); } + #endregion - - #region Linq Wrapping Extensions + #region ToContentSet - //NOTE: These are all purely required to fix this issue: http://issues.umbraco.org/issue/U4-1797 which requires that any - // content item knows about it's containing collection. - - public static IEnumerable Where(this IEnumerable source, Func predicate) + /// + /// Returns the content enumerable as a content set. + /// + /// The content enumerable. + /// A content set wrapping the content enumerable. + public static PublishedContentSet ToContentSet(this IEnumerable source) + where T : class, IPublishedContent { - var internalResult = Enumerable.Where(source, predicate); - return new DynamicPublishedContentList(internalResult); + return new PublishedContentSet(source); } - public static IEnumerable Where(this IEnumerable source, Func predicate) + /// + /// Returns the ordered content enumerable as an ordered content set. + /// + /// The ordered content enumerable. + /// A ordered content set wrapping the ordered content enumerable. + public static PublishedContentOrderedSet ToContentSet(this IOrderedEnumerable source) + where T : class, IPublishedContent { - var internalResult = Enumerable.Where(source, predicate); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Take(this IEnumerable source, int count) - { - var internalResult = Enumerable.Take(source, count); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable TakeWhile(this IEnumerable source, Func predicate) - { - var internalResult = Enumerable.TakeWhile(source, predicate); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable TakeWhile(this IEnumerable source, Func predicate) - { - var internalResult = Enumerable.TakeWhile(source, predicate); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Skip(this IEnumerable source, int count) - { - var internalResult = Enumerable.Skip(source, count); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable SkipWhile(this IEnumerable source, Func predicate) - { - var internalResult = Enumerable.SkipWhile(source, predicate); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable SkipWhile(this IEnumerable source, Func predicate) - { - var internalResult = Enumerable.SkipWhile(source, predicate); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Concat(this IEnumerable first, IEnumerable second) - { - var internalResult = Enumerable.Concat(first, second); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Distinct(this IEnumerable source) - { - var internalResult = Enumerable.Distinct(source); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Distinct(this IEnumerable source, IEqualityComparer comparer) - { - var internalResult = Enumerable.Distinct(source, comparer); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Union(this IEnumerable first, IEnumerable second) - { - var internalResult = Enumerable.Union(first, second); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Union(this IEnumerable first, IEnumerable second, IEqualityComparer comparer) - { - var internalResult = Enumerable.Union(first, second, comparer); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Intersect(this IEnumerable first, IEnumerable second) - { - var internalResult = Enumerable.Intersect(first, second); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Intersect(this IEnumerable first, IEnumerable second, IEqualityComparer comparer) - { - var internalResult = Enumerable.Intersect(first, second, comparer); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Except(this IEnumerable first, IEnumerable second) - { - var internalResult = Enumerable.Except(first, second); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Except(this IEnumerable first, IEnumerable second, IEqualityComparer comparer) - { - var internalResult = Enumerable.Except(first, second, comparer); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable Reverse(this IEnumerable source) - { - var internalResult = Enumerable.Reverse(source); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable DefaultIfEmpty(this IEnumerable source) - { - var internalResult = Enumerable.DefaultIfEmpty(source); - return new DynamicPublishedContentList(internalResult); - } - - public static IEnumerable DefaultIfEmpty(this IEnumerable source, IPublishedContent defaultValue) - { - var internalResult = Enumerable.DefaultIfEmpty(source, defaultValue); - return new DynamicPublishedContentList(internalResult); + return new PublishedContentOrderedSet(source); } #endregion - #region Dynamic Linq Extensions - public static IQueryable OrderBy(this IEnumerable list, string predicate) + // todo - we should keep this file clean and remove dynamic linq stuff from it + + public static IQueryable OrderBy(this IEnumerable source, string predicate) { - var dList = new DynamicPublishedContentList(list); + var dList = new DynamicPublishedContentList(source); return dList.OrderBy(predicate); } public static IQueryable Where(this IEnumerable list, string predicate) { - var dList = new DynamicPublishedContentList(list); - //we have to wrap the result in another DynamicPublishedContentList so that the OwnersList get's set on - //the individual items. See: http://issues.umbraco.org/issue/U4-1797 - return new DynamicPublishedContentList( - dList.Where(predicate)) - .AsQueryable(); + // wrap in DynamicPublishedContentList so that the ContentSet is correct + // though that code is somewhat ugly. + + var dlist = new DynamicPublishedContentList(new DynamicPublishedContentList(list) + .Where(predicate)); + + return dlist.AsQueryable(); } public static IEnumerable> GroupBy(this IEnumerable list, string predicate) @@ -439,363 +498,391 @@ namespace Umbraco.Web return dList.Select(predicate); } - #endregion + public static HtmlString Where(this IPublishedContent content, string predicate, string valueIfTrue) + { + if (content == null) throw new ArgumentNullException("content"); + return content.Where(predicate, valueIfTrue, string.Empty); + } - public static dynamic AsDynamic(this IPublishedContent doc) - { - if (doc == null) throw new ArgumentNullException("doc"); - var dd = new DynamicPublishedContent(doc); - return dd.AsDynamic(); - } + public static HtmlString Where(this IPublishedContent content, string predicate, string valueIfTrue, string valueIfFalse) + { + if (content == null) throw new ArgumentNullException("content"); + return new HtmlString(content.Where(predicate) ? valueIfTrue : valueIfFalse); + } - /// - /// Converts a IPublishedContent to a DynamicPublishedContent and tests for null - /// - /// - /// - internal static DynamicPublishedContent AsDynamicPublishedContent(this IPublishedContent content) + public static bool Where(this IPublishedContent content, string predicate) + { + if (content == null) throw new ArgumentNullException("content"); + var dynamicDocumentList = new DynamicPublishedContentList { content.AsDynamicOrNull() }; + var filtered = dynamicDocumentList.Where(predicate); + return filtered.Count() == 1; + } + + #endregion + + #region AsDynamic + + // it is ok to have dynamic here + + // content should NOT be null + public static dynamic AsDynamic(this IPublishedContent content) { - if (content == null) - return null; + if (content == null) throw new ArgumentNullException("content"); return new DynamicPublishedContent(content); } - #region Where - - public static HtmlString Where(this IPublishedContent doc, string predicate, string valueIfTrue) + // content CAN be null + internal static DynamicPublishedContent AsDynamicOrNull(this IPublishedContent content) { - if (doc == null) throw new ArgumentNullException("doc"); - return doc.Where(predicate, valueIfTrue, string.Empty); + return content == null ? null : new DynamicPublishedContent(content); } - public static HtmlString Where(this IPublishedContent doc, string predicate, string valueIfTrue, string valueIfFalse) - { - if (doc == null) throw new ArgumentNullException("doc"); - if (doc.Where(predicate)) - { - return new HtmlString(valueIfTrue); - } - return new HtmlString(valueIfFalse); - } - - public static bool Where(this IPublishedContent doc, string predicate) - { - if (doc == null) throw new ArgumentNullException("doc"); - var dynamicDocumentList = new DynamicPublishedContentList(); - dynamicDocumentList.Add(doc.AsDynamicPublishedContent()); - var filtered = dynamicDocumentList.Where(predicate); - if (filtered.Count() == 1) - { - //this node matches the predicate - return true; - } - return false; - } + #endregion - #endregion + #region ContentSet - #region Position/Index public static int Position(this IPublishedContent content) { - return content.Index(); - } - public static int Index(this IPublishedContent content) - { - var container = content.GetOwnersList().ToList(); - int currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - return currentIndex; - } - else - { - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicDocumentList but could not retrieve the index for it's position in the list", content.Id)); - } + return content.GetIndex(); } - /// - /// Return the owners collection of the current content item. - /// - /// - /// - /// - /// If the content item is of type PublishedContentBase we will have a property called OwnersCollection which will - /// be the collection of a resultant set (i.e. from a where clause, a call to Children(), etc...) otherwise it will - /// be the item's siblings. All relates to this issue: http://issues.umbraco.org/issue/U4-1797 - /// - private static IEnumerable GetOwnersList(this IPublishedContent content) + public static int Index(this IPublishedContent content) { - //Here we need to type check, we need to see if we have a real OwnersCollection list based on the result set - // of a query, otherwise we can only lookup among the item's siblings. All related to this issue here: - // http://issues.umbraco.org/issue/U4-1797 + return content.GetIndex(); + } - var publishedContentBase = content as IOwnerCollectionAware; - var ownersList = publishedContentBase != null - ? publishedContentBase.OwnersCollection - : content.Siblings(); - return ownersList; - } + private static int GetIndex(this IPublishedContent content, IEnumerable set) + { + var index = set.FindIndex(n => n.Id == content.Id); + if (index < 0) + throw new IndexOutOfRangeException("Could not find content in the content set."); + return index; + } #endregion - #region Is Helpers + #region IsSomething: misc. + + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// A value indicating whether the content is visible. + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + public static bool IsVisible(this IPublishedContent content) + { + // note: would be better to ensure we have an IPropertyEditorValueConverter for booleans + // and then treat the umbracoNaviHide property as a boolean - vs. the hard-coded "1". + + // rely on the property converter - will return default bool value, ie false, if property + // is not defined, or has no value, else will return its value. + return content.GetPropertyValue(Constants.Conventions.Content.NaviHide) == false; + } public static bool IsDocumentType(this IPublishedContent content, string docTypeAlias) { return content.DocumentTypeAlias == docTypeAlias; } - public static bool IsNull(this IPublishedContent content, string alias, bool recursive) + public static bool IsNull(this IPublishedContent content, string alias, bool recurse) { - var prop = content.GetProperty(alias, recursive); - if (prop == null) return true; - return ((PropertyResult)prop).HasValue(); + return content.HasValue(alias, recurse) == false; } + public static bool IsNull(this IPublishedContent content, string alias) { - return content.IsNull(alias, false); - } + return content.HasValue(alias) == false; + } - #region Position in list + #endregion + + #region IsSomething: position in set public static bool IsFirst(this IPublishedContent content) { - return content.IsHelper(n => n.Index() == 0); + return content.GetIndex() == 0; } + public static HtmlString IsFirst(this IPublishedContent content, string valueIfTrue) { - return content.IsHelper(n => n.Index() == 0, valueIfTrue); + return content.IsFirst(valueIfTrue, string.Empty); } + public static HtmlString IsFirst(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() == 0, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsFirst() ? valueIfTrue : valueIfFalse); } + public static bool IsNotFirst(this IPublishedContent content) { - return !content.IsHelper(n => n.Index() == 0); + return content.IsFirst() == false; } + public static HtmlString IsNotFirst(this IPublishedContent content, string valueIfTrue) { - return content.IsHelper(n => n.Index() != 0, valueIfTrue); + return content.IsNotFirst(valueIfTrue, string.Empty); } + public static HtmlString IsNotFirst(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() != 0, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsNotFirst() ? valueIfTrue : valueIfFalse); } + public static bool IsPosition(this IPublishedContent content, int index) { - return content.IsHelper(n => n.Index() == index); + return content.GetIndex() == index; } + public static HtmlString IsPosition(this IPublishedContent content, int index, string valueIfTrue) { - return content.IsHelper(n => n.Index() == index, valueIfTrue); + return content.IsPosition(index, valueIfTrue, string.Empty); } + public static HtmlString IsPosition(this IPublishedContent content, int index, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() == index, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsPosition(index) ? valueIfTrue : valueIfFalse); } + public static bool IsModZero(this IPublishedContent content, int modulus) { - return content.IsHelper(n => n.Index() % modulus == 0); + return content.GetIndex() % modulus == 0; } + public static HtmlString IsModZero(this IPublishedContent content, int modulus, string valueIfTrue) { - return content.IsHelper(n => n.Index() % modulus == 0, valueIfTrue); + return content.IsModZero(modulus, valueIfTrue, string.Empty); } + public static HtmlString IsModZero(this IPublishedContent content, int modulus, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() % modulus == 0, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsModZero(modulus) ? valueIfTrue : valueIfFalse); } + public static bool IsNotModZero(this IPublishedContent content, int modulus) { - return content.IsHelper(n => n.Index() % modulus != 0); + return content.IsModZero(modulus) == false; } + public static HtmlString IsNotModZero(this IPublishedContent content, int modulus, string valueIfTrue) { - return content.IsHelper(n => n.Index() % modulus != 0, valueIfTrue); + return content.IsNotModZero(modulus, valueIfTrue, string.Empty); } + public static HtmlString IsNotModZero(this IPublishedContent content, int modulus, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() % modulus != 0, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsNotModZero(modulus) ? valueIfTrue : valueIfFalse); } + public static bool IsNotPosition(this IPublishedContent content, int index) { - return !content.IsHelper(n => n.Index() == index); + return content.IsPosition(index) == false; } + public static HtmlString IsNotPosition(this IPublishedContent content, int index, string valueIfTrue) { - return content.IsHelper(n => n.Index() != index, valueIfTrue); + return content.IsNotPosition(index, valueIfTrue, string.Empty); } + public static HtmlString IsNotPosition(this IPublishedContent content, int index, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() != index, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsNotPosition(index) ? valueIfTrue : valueIfFalse); } + public static bool IsLast(this IPublishedContent content) { - var ownersList = content.GetOwnersList(); - var count = ownersList.Count(); - return content.IsHelper(n => n.Index() == count - 1); + return content.GetIndex() == content.ContentSet.Count() - 1; } + public static HtmlString IsLast(this IPublishedContent content, string valueIfTrue) { - var ownersList = content.GetOwnersList(); - var count = ownersList.Count(); - return content.IsHelper(n => n.Index() == count - 1, valueIfTrue); + return content.IsLast(valueIfTrue, string.Empty); } + public static HtmlString IsLast(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { - var ownersList = content.GetOwnersList(); - var count = ownersList.Count(); - return content.IsHelper(n => n.Index() == count - 1, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsLast() ? valueIfTrue : valueIfFalse); } + public static bool IsNotLast(this IPublishedContent content) { - var ownersList = content.GetOwnersList(); - var count = ownersList.Count(); - return !content.IsHelper(n => n.Index() == count - 1); + return content.IsLast() == false; } + public static HtmlString IsNotLast(this IPublishedContent content, string valueIfTrue) { - var ownersList = content.GetOwnersList(); - var count = ownersList.Count(); - return content.IsHelper(n => n.Index() != count - 1, valueIfTrue); + return content.IsNotLast(valueIfTrue, string.Empty); } + public static HtmlString IsNotLast(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { - var ownersList = content.GetOwnersList(); - var count = ownersList.Count(); - return content.IsHelper(n => n.Index() != count - 1, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsNotLast() ? valueIfTrue : valueIfFalse); } + public static bool IsEven(this IPublishedContent content) { - return content.IsHelper(n => n.Index() % 2 == 0); + return content.GetIndex() % 2 == 0; } + public static HtmlString IsEven(this IPublishedContent content, string valueIfTrue) { - return content.IsHelper(n => n.Index() % 2 == 0, valueIfTrue); + return content.IsEven(valueIfTrue, string.Empty); } + public static HtmlString IsEven(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() % 2 == 0, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsEven() ? valueIfTrue : valueIfFalse); } + public static bool IsOdd(this IPublishedContent content) { - return content.IsHelper(n => n.Index() % 2 == 1); + return content.GetIndex() % 2 == 1; } + public static HtmlString IsOdd(this IPublishedContent content, string valueIfTrue) { - return content.IsHelper(n => n.Index() % 2 == 1, valueIfTrue); + return content.IsOdd(valueIfTrue, string.Empty); } + public static HtmlString IsOdd(this IPublishedContent content, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Index() % 2 == 1, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsOdd() ? valueIfTrue : valueIfFalse); } + #endregion - + + #region IsSomething: equality + public static bool IsEqual(this IPublishedContent content, IPublishedContent other) { - return content.IsHelper(n => n.Id == other.Id); - } - public static HtmlString IsEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) - { - return content.IsHelper(n => n.Id == other.Id, valueIfTrue); + return content.Id == other.Id; } + + public static HtmlString IsEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) + { + return content.IsEqual(other, valueIfTrue, string.Empty); + } + public static HtmlString IsEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Id == other.Id, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsEqual(other) ? valueIfTrue : valueIfFalse); } + public static bool IsNotEqual(this IPublishedContent content, IPublishedContent other) { - return content.IsHelper(n => n.Id != other.Id); + return content.IsEqual(other) == false; } - public static HtmlString IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) + + public static HtmlString IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue) + { + return content.IsNotEqual(other, valueIfTrue, string.Empty); + } + + public static HtmlString IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { - return content.IsHelper(n => n.Id != other.Id, valueIfTrue); + return new HtmlString(content.IsNotEqual(other) ? valueIfTrue : valueIfFalse); } - public static HtmlString IsNotEqual(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) - { - return content.IsHelper(n => n.Id != other.Id, valueIfTrue, valueIfFalse); - } - public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) - { - var ancestors = content.Ancestors(); - return content.IsHelper(n => ancestors.FirstOrDefault(ancestor => ancestor.Id == other.Id) != null); + + #endregion + + #region IsSomething: ancestors and descendants + + public static bool IsDescendant(this IPublishedContent content, IPublishedContent other) + { + return content.Ancestors().Any(x => x.Id == other.Id); } + public static HtmlString IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue) { - var ancestors = content.Ancestors(); - return content.IsHelper(n => ancestors.FirstOrDefault(ancestor => ancestor.Id == other.Id) != null, valueIfTrue); + return content.IsDescendant(other, valueIfTrue, string.Empty); } + public static HtmlString IsDescendant(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { - var ancestors = content.Ancestors(); - return content.IsHelper(n => ancestors.FirstOrDefault(ancestor => ancestor.Id == other.Id) != null, valueIfTrue, valueIfFalse); + return new HtmlString(content.IsDescendant(other) ? valueIfTrue : valueIfFalse); } + public static bool IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other) { - var ancestors = content.AncestorsOrSelf(); - return content.IsHelper(n => ancestors.FirstOrDefault(ancestor => ancestor.Id == other.Id) != null); - } + return content.AncestorsOrSelf().Any(x => x.Id == other.Id); + } + public static HtmlString IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) { - var ancestors = content.AncestorsOrSelf(); - return content.IsHelper(n => ancestors.FirstOrDefault(ancestor => ancestor.Id == other.Id) != null, valueIfTrue); - } + return content.IsDescendantOrSelf(other, valueIfTrue, string.Empty); + } + public static HtmlString IsDescendantOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { - var ancestors = content.AncestorsOrSelf(); - return content.IsHelper(n => ancestors.FirstOrDefault(ancestor => ancestor.Id == other.Id) != null, valueIfTrue, valueIfFalse); - } + return new HtmlString(content.IsDescendantOrSelf(other) ? valueIfTrue : valueIfFalse); + } + public static bool IsAncestor(this IPublishedContent content, IPublishedContent other) { - var descendants = content.Descendants(); - return content.IsHelper(n => descendants.FirstOrDefault(descendant => descendant.Id == other.Id) != null); + // avoid using Descendants(), that's expensive + return other.Ancestors().Any(x => x.Id == content.Id); } + public static HtmlString IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue) { - var descendants = content.Descendants(); - return content.IsHelper(n => descendants.FirstOrDefault(descendant => descendant.Id == other.Id) != null, valueIfTrue); - } + return content.IsAncestor(other, valueIfTrue, string.Empty); + } + public static HtmlString IsAncestor(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { - var descendants = content.Descendants(); - return content.IsHelper(n => descendants.FirstOrDefault(descendant => descendant.Id == other.Id) != null, valueIfTrue, valueIfFalse); - } + return new HtmlString(content.IsAncestor(other) ? valueIfTrue : valueIfFalse); + } + public static bool IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other) { - var descendants = content.DescendantsOrSelf(); - return content.IsHelper(n => descendants.FirstOrDefault(descendant => descendant.Id == other.Id) != null); - } + // avoid using DescendantsOrSelf(), that's expensive + return other.AncestorsOrSelf().Any(x => x.Id == content.Id); + } + public static HtmlString IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue) { - var descendants = content.DescendantsOrSelf(); - return content.IsHelper(n => descendants.FirstOrDefault(descendant => descendant.Id == other.Id) != null, valueIfTrue); - } + return content.IsAncestorOrSelf(other, valueIfTrue, string.Empty); + } + public static HtmlString IsAncestorOrSelf(this IPublishedContent content, IPublishedContent other, string valueIfTrue, string valueIfFalse) { - var descendants = content.DescendantsOrSelf(); - return content.IsHelper(n => descendants.FirstOrDefault(descendant => descendant.Id == other.Id) != null, valueIfTrue, valueIfFalse); - } - private static bool IsHelper(this IPublishedContent content, Func test) - { - return test(content); - } - private static HtmlString IsHelper(this IPublishedContent content, Func test, string valueIfTrue) - { - return content.IsHelper(test, valueIfTrue, string.Empty); - } - private static HtmlString IsHelper(this IPublishedContent content, Func test, string valueIfTrue, string valueIfFalse) - { - return test(content) ? new HtmlString(valueIfTrue) : new HtmlString(valueIfFalse); - } + return new HtmlString(content.IsAncestorOrSelf(other) ? valueIfTrue : valueIfFalse); + } - #endregion + #endregion - #region Ancestors + #region Axes: ancestors, ancestors-or-self + + // as per XPath 1.0 specs §2.2, + // - the ancestor axis contains the ancestors of the context node; the ancestors of the context node consist + // of the parent of context node and the parent's parent and so on; thus, the ancestor axis will always + // include the root node, unless the context node is the root node. + // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, + // the ancestor axis will always include the root node. + // + // as per XPath 2.0 specs §3.2.1.1, + // - the ancestor axis is defined as the transitive closure of the parent axis; it contains the ancestors + // of the context node (the parent, the parent of the parent, and so on) - The ancestor axis includes the + // root node of the tree in which the context node is found, unless the context node is the root node. + // - the ancestor-or-self axis contains the context node and the ancestors of the context node; thus, + // the ancestor-or-self axis will always include the root node. + // + // the ancestor and ancestor-or-self axis are reverse axes ie they contain the context node or nodes that + // are before the context node in document order. + // + // document order is defined by §2.4.1 as: + // - the root node is the first node. + // - every node occurs before all of its children and descendants. + // - the relative order of siblings is the order in which they occur in the children property of their parent node. + // - children and descendants occur before following siblings. + + // SO, here we want to walk up the tree. which is what AncestorOrSelf does but NOT what AncestorsOrSelf does since + // it reverses the list, so basically ancestors are NOT XPath-compliant in Umbraco at the moment -- but fixing that + // would be a breaking change. Defining FIX_AXES would fix the situation. public static IEnumerable Ancestors(this IPublishedContent content) { - return content.AncestorsOrSelf(false, n => true); + return content.AncestorsOrSelf(false, null); } public static IEnumerable Ancestors(this IPublishedContent content, int level) @@ -803,51 +890,26 @@ namespace Umbraco.Web return content.AncestorsOrSelf(false, n => n.Level <= level); } - public static IEnumerable Ancestors(this IPublishedContent content, string nodeTypeAlias) + public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(false, n => n.DocumentTypeAlias == nodeTypeAlias); + return content.AncestorsOrSelf(false, n => n.DocumentTypeAlias == contentTypeAlias); } - internal static IEnumerable Ancestors(this IPublishedContent content, Func func) + public static IEnumerable Ancestors(this IPublishedContent content) + where T : class, IPublishedContent { - return content.AncestorsOrSelf(false, func); + return content.Ancestors().OfType(); } - public static IPublishedContent AncestorOrSelf(this IPublishedContent content) + public static IEnumerable Ancestors(this IPublishedContent content, int level) + where T : class, IPublishedContent { - //TODO: Why is this query like this?? - return content.AncestorOrSelf(node => node.Level == 1); - } - - public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int level) - { - return content.AncestorOrSelf(node => node.Level == level); - } - - public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string nodeTypeAlias) - { - return content.AncestorOrSelf(node => node.DocumentTypeAlias == nodeTypeAlias); - } - - internal static IPublishedContent AncestorOrSelf(this IPublishedContent content, Func func) - { - if (func(content)) - return content; - - while (content.Level > 1) // while we have a parent, consider the parent - { - content = content.Parent; - - if (func(content)) - return content; - } - - return null; + return content.Ancestors(level).OfType(); } public static IEnumerable AncestorsOrSelf(this IPublishedContent content) { - return content.AncestorsOrSelf(true, n => true); + return content.AncestorsOrSelf(true, null); } public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int level) @@ -855,307 +917,677 @@ namespace Umbraco.Web return content.AncestorsOrSelf(true, n => n.Level <= level); } - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string nodeTypeAlias) + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(true, n => n.DocumentTypeAlias == nodeTypeAlias); + return content.AncestorsOrSelf(true, n => n.DocumentTypeAlias == contentTypeAlias); } - internal static IEnumerable AncestorsOrSelf(this IPublishedContent content, Func func) - { - return content.AncestorsOrSelf(true, func); - } - - internal static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func func) + public static IEnumerable AncestorsOrSelf(this IPublishedContent content) + where T : class, IPublishedContent { + return content.AncestorsOrSelf().OfType(); + } + + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.AncestorsOrSelf(level).OfType(); + } + + public static IPublishedContent Ancestor(this IPublishedContent content) + { + return content.Parent; + } + + public static IPublishedContent Ancestor(this IPublishedContent content, int level) + { + return content.EnumerateAncestors(false).FirstOrDefault(x => x.Level <= level); + } + + public static IPublishedContent Ancestor(this IPublishedContent content, string contentTypeAlias) + { + return content.EnumerateAncestors(false).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + } + + public static T Ancestor(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Ancestor() as T; + } + + public static T Ancestor(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.Ancestor(level) as T; + } + + // note: that one makes no sense and should return self -- but fixing that + // would be a breaking change. Defining FIX_AXES would fix the situation. + public static IPublishedContent AncestorOrSelf(this IPublishedContent content) + { +#if FIX_AXES + return content; +#else + return content.EnumerateAncestors(true).FirstOrDefault(x => x.Level == 1); +#endif + } + + public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int level) + { + return content.EnumerateAncestors(true).FirstOrDefault(x => x.Level <= level); + } + + public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) + { + return content.EnumerateAncestors(true).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + } + + public static T AncestorOrSelf(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.AncestorOrSelf() as T; + } + + public static T AncestorOrSelf(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.AncestorOrSelf(level) as T; + } + + // broken until we defined FIX_AXES + internal static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func func) + { +#if FIX_AXES + return content.EnumerateAncestors(orSelf).Where(x => func == null || func(x)); +#else var ancestors = new List(); - if (orSelf && func(content)) + if (orSelf && (func == null || func(content))) ancestors.Add(content); while (content.Level > 1) // while we have a parent, consider the parent { content = content.Parent; - if (func(content)) + if ((func == null || func(content))) ancestors.Add(content); } ancestors.Reverse(); return ancestors; +#endif + } + + internal static IEnumerable EnumerateAncestors(this IPublishedContent content, bool orSelf) + { + if (orSelf) yield return content; + while ((content = content.Parent) != null) + yield return content; } #endregion - #region Descendants - public static IEnumerable Descendants(this IPublishedContent content, string nodeTypeAlias) - { - return content.Descendants(p => p.DocumentTypeAlias == nodeTypeAlias); - } - public static IEnumerable Descendants(this IPublishedContent content, int level) - { - return content.Descendants(p => p.Level >= level); - } - public static IEnumerable Descendants(this IPublishedContent content) - { - return content.Descendants(n => true); - } - private static IEnumerable Descendants(this IPublishedContent content, Func func) - { - //return content.Children.Map(func, (IPublishedContent n) => n.Children); - return content.Children.FlattenList(x => x.Children).Where(func) - .OrderBy(x => x.Level) //ensure its sorted by level and then by sort order - .ThenBy(x => x.SortOrder); - } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level) - { - return content.DescendantsOrSelf(p => p.Level >= level); - } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string nodeTypeAlias) - { - return content.DescendantsOrSelf(p => p.DocumentTypeAlias == nodeTypeAlias); - } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content) - { - return content.DescendantsOrSelf(p => true); - } - internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, Func func) - { - if (content != null) - { - var thisNode = new List(); - if (func(content)) - { - thisNode.Add(content); - } - //var flattenedNodes = content.Children.Map(func, (IPublishedContent n) => n.Children); - var flattenedNodes = content.Children.FlattenList(n => n.Children).Where(func); + #region Axes: descendants, descendants-or-self - return thisNode.Concat(flattenedNodes) - .Select(dynamicBackingItem => new DynamicPublishedContent(dynamicBackingItem)) - .OrderBy(x => x.Level) //ensure its sorted by level and then by sort order - .ThenBy(x => x.SortOrder); - } - return Enumerable.Empty(); - } - #endregion + // as per XPath 1.0 specs §2.2, + // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus + // the descendant axis never contains attribute or namespace nodes. + // - the descendant-or-self axis contains the context node and the descendants of the context node. + // + // as per XPath 2.0 specs §3.2.1.1, + // - the descendant axis is defined as the transitive closure of the child axis; it contains the descendants of the context node (the + // children, the children of the children, and so on). + // - the descendant-or-self axis contains the context node and the descendants of the context node. + // + // the descendant and descendant-or-self axis are forward axes ie they contain the context node or nodes that are after the context + // node in document order. + // + // document order is defined by §2.4.1 as: + // - the root node is the first node. + // - every node occurs before all of its children and descendants. + // - the relative order of siblings is the order in which they occur in the children property of their parent node. + // - children and descendants occur before following siblings. + + // SO, here we want to implement a depth-first enumeration of children. Which is what EnumerateDescendants does, but NOT what + // DescendantsOrSelf does, so basically descendants are NOT XPath-compliant in Umbraco at the moment -- but fixing that + // would be a breaking change. Defining FIX_AXES would fix the situation. - #region Traversal + public static IEnumerable Descendants(this IPublishedContent content) + { + return content.DescendantsOrSelf(false, null); + } + + public static IEnumerable Descendants(this IPublishedContent content, int level) + { + return content.DescendantsOrSelf(false, p => p.Level >= level); + } + + public static IEnumerable Descendants(this IPublishedContent content, string contentTypeAlias) + { + return content.DescendantsOrSelf(false, p => p.DocumentTypeAlias == contentTypeAlias); + } + + public static IEnumerable Descendants(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Descendants().OfType(); + } + + public static IEnumerable Descendants(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.Descendants(level).OfType(); + } + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content) + { + return content.DescendantsOrSelf(true, null); + } + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level) + { + return content.DescendantsOrSelf(true, p => p.Level >= level); + } + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string contentTypeAlias) + { + return content.DescendantsOrSelf(true, p => p.DocumentTypeAlias == contentTypeAlias); + } + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.DescendantsOrSelf().OfType(); + } + + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.DescendantsOrSelf(level).OfType(); + } + + public static IPublishedContent Descendant(this IPublishedContent content) + { + return content.Children.FirstOrDefault(); + } + + public static IPublishedContent Descendant(this IPublishedContent content, int level) + { + return content.EnumerateDescendants(false).FirstOrDefault(x => x.Level == level); + } + + public static IPublishedContent Descendant(this IPublishedContent content, string contentTypeAlias) + { + return content.EnumerateDescendants(false).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + } + + public static T Descendant(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.EnumerateDescendants(false).FirstOrDefault(x => x is T) as T; + } + + public static T Descendant(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.Descendant(level) as T; + } + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content) + { + return content; + } + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, int level) + { + return content.EnumerateDescendants(true).FirstOrDefault(x => x.Level == level); + } + + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string contentTypeAlias) + { + return content.EnumerateDescendants(true).FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAlias); + } + + public static T DescendantOrSelf(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.EnumerateDescendants(true).FirstOrDefault(x => x is T) as T; + } + + public static T DescendantOrSelf(this IPublishedContent content, int level) + where T : class, IPublishedContent + { + return content.DescendantOrSelf(level) as T; + } + + // broken until we defined FIX_AXES + internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, bool orSelf, Func func) + { +#if FIX_AXES + return content.EnumerateDescendants(orSelf).Where(x => func == null || func(x)); +#else + var init = (orSelf && (func == null || func(content))) ? new[] { content } : new IPublishedContent[] { }; + + var descendants = init + .Union(content.Children + .FlattenList(x => x.Children) + .Where(x => func == null || func(x)) + ) + .OrderBy(x => x.Level) + .ThenBy(x => x.SortOrder); + + return descendants; +#endif + } + + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, bool orSelf) + { + if (orSelf) yield return content; + + foreach (var child in content.Children) + foreach (var child2 in child.EnumerateDescendants()) + yield return child2; + } + + internal static IEnumerable EnumerateDescendants(this IPublishedContent content) + { + yield return content; + + foreach (var child in content.Children) + foreach (var child2 in child.EnumerateDescendants()) + yield return child2; + } + + #endregion + + #region Axes: following-sibling, preceding-sibling, following, preceding + pseudo-axes up, down, next, previous + + // up pseudo-axe ~ ancestors + // bogus, kept for backward compatibility but we should get rid of it + // better use ancestors public static IPublishedContent Up(this IPublishedContent content) { - return content.Up(0); + return content.Parent; } + public static IPublishedContent Up(this IPublishedContent content, int number) { - if (number == 0) - { - return content.Parent; - } - while ((content = content.Parent) != null && --number >= 0) ; - return content; + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); +#if (!FIX_AXES) + number += 1; // legacy is zero-based ie zero == parent +#endif + return number == 0 ? content : content.EnumerateAncestors(false).Skip(number).FirstOrDefault(); } - public static IPublishedContent Up(this IPublishedContent content, string nodeTypeAlias) + + public static IPublishedContent Up(this IPublishedContent content, string contentTypeAlias) { - if (string.IsNullOrEmpty(nodeTypeAlias)) - { - return content.Parent; - } - while ((content = content.Parent) != null && content.DocumentTypeAlias != nodeTypeAlias) ; - return content; + return string.IsNullOrEmpty(contentTypeAlias) + ? content.Parent + : content.Ancestor(contentTypeAlias); } + + // down pseudo-axe ~ children (not descendants) + // bogus, kept for backward compatibility but we should get rid of it + // better use descendants + public static IPublishedContent Down(this IPublishedContent content) { - return content.Down(0); + return content.Children.FirstOrDefault(); } + public static IPublishedContent Down(this IPublishedContent content, int number) { - var children = content.Children; - if (number == 0) - { - return children.First(); - } - var working = content; - while (number-- >= 0) - { - working = children.First(); - children = new DynamicPublishedContentList(working.Children); - } - return working; + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); +#if (!FIX_AXES) + number += 1; // legacy is zero-based ie zero == first child +#endif + if (number == 0) return content; + + content = content.Children.FirstOrDefault(); + while (content != null && --number > 0) + content = content.Children.FirstOrDefault(); + + return content; } - public static IPublishedContent Down(this IPublishedContent content, string nodeTypeAlias) + + public static IPublishedContent Down(this IPublishedContent content, string contentTypeAlias) { - if (string.IsNullOrEmpty(nodeTypeAlias)) - { - var children = content.Children; - return children.First(); - } - return content.Descendants(nodeTypeAlias).FirstOrDefault(); + if (string.IsNullOrEmpty(contentTypeAlias)) + return content.Children.FirstOrDefault(); + + // note: this is what legacy did, but with a broken Descendant + // so fixing Descendant will change how it works... + return content.Descendant(contentTypeAlias); } + // next pseudo-axe ~ following within the content set + // bogus, kept for backward compatibility but we should get rid of it + public static IPublishedContent Next(this IPublishedContent content) { - return content.Next(0); - } - public static IPublishedContent Next(this IPublishedContent content, int number) - { - var ownersList = content.GetOwnersList(); + return content.ContentSet.ElementAtOrDefault(content.GetIndex() + 1); + } - var container = ownersList.ToList(); - var currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - return container.ElementAtOrDefault(currentIndex + (number + 1)); - } - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicNodeList but could not retrieve the index for it's position in the list", content.Id)); - } - - public static IPublishedContent Next(this IPublishedContent content, string nodeTypeAlias) + public static IPublishedContent Next(this IPublishedContent content, int number) { - var ownersList = content.GetOwnersList(); + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); +#if (!FIX_AXES) + number += 1; // legacy is zero-based ie zero == next, whereas zero should be current +#endif + return number == 0 ? content : content.ContentSet.ElementAtOrDefault(content.GetIndex() + number); + } - var container = ownersList.ToList(); - var currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - var newIndex = container.FindIndex(currentIndex, n => n.DocumentTypeAlias == nodeTypeAlias); - return newIndex != -1 - ? container.ElementAt(newIndex) - : null; - } - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicNodeList but could not retrieve the index for it's position in the list", content.Id)); - } - public static IPublishedContent Previous(this IPublishedContent content) + public static IPublishedContent Next(this IPublishedContent content, string contentTypeAlias) + { + return content.Next(contentTypeAlias, false); + } + + public static IPublishedContent Next(this IPublishedContent content, string contentTypeAlias, bool wrap) + { + return content.Next(content.ContentSet, x => x.DocumentTypeAlias.InvariantEquals(contentTypeAlias), wrap); + } + + public static T Next(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Next(false); + } + + public static T Next(this IPublishedContent content, bool wrap) + where T : class, IPublishedContent + { + return content.Next(content.ContentSet, x => x is T, wrap) as T; + } + + static IPublishedContent Next(this IPublishedContent content, IEnumerable axis, Func predicate, bool wrap) + { + var b4 = true; + IPublishedContent wrapped = null; + foreach (var c in axis) + { + if (b4) + { + if (c == content) + b4 = false; + else if (wrap && wrapped == null && predicate(c)) + wrapped = c; + continue; + } + if (predicate(c)) + return c; + } + + return wrapped; + } + + // previous pseudo-axe ~ preceding within the content set + // bogus, kept for backward compatibility but we should get rid of it + + public static IPublishedContent Previous(this IPublishedContent content) { - return content.Previous(0); - } + return content.ContentSet.ElementAtOrDefault(content.GetIndex() - 1); + } + public static IPublishedContent Previous(this IPublishedContent content, int number) { - var ownersList = content.GetOwnersList(); + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); +#if (!FIX_AXES) + number = -number; // legacy wants negative numbers, should be positive + number += 1; // legacy is zero-based ie zero == previous, whereas zero should be current +#endif + return number == 0 ? content : content.ContentSet.ElementAtOrDefault(content.GetIndex() - number); + } - var container = ownersList.ToList(); - var currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - return container.ElementAtOrDefault(currentIndex + (number - 1)); - } - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicNodeList but could not retrieve the index for it's position in the list", content.Id)); - } - public static IPublishedContent Previous(this IPublishedContent content, string nodeTypeAlias) + public static IPublishedContent Previous(this IPublishedContent content, string contentTypeAlias) { - var ownersList = content.GetOwnersList(); - - var container = ownersList.ToList(); - int currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - var previousNodes = container.Take(currentIndex).ToList(); - int newIndex = previousNodes.FindIndex(n => n.DocumentTypeAlias == nodeTypeAlias); - if (newIndex != -1) - { - return container.ElementAt(newIndex); - } - return null; - } - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicNodeList but could not retrieve the index for it's position in the list", content.Id)); + return content.Previous(contentTypeAlias, false); } + + public static IPublishedContent Previous(this IPublishedContent content, string contentTypeAlias, bool wrap) + { + return content.Next(content.ContentSet.Reverse(), x => x.DocumentTypeAlias.InvariantEquals(contentTypeAlias), wrap); + } + + public static T Previous(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Previous(false); + } + + public static T Previous(this IPublishedContent content, bool wrap) + where T : class, IPublishedContent + { + return content.Next(content.ContentSet.Reverse(), x => x is T, wrap) as T; + } + + // + + [Obsolete("Obsolete, use FollowingSibling or PrecedingSibling instead.")] public static IPublishedContent Sibling(this IPublishedContent content, int number) - { - var siblings = content.Siblings(); - - var container = siblings.ToList(); - var currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - return container.ElementAtOrDefault(currentIndex + number); - } - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicNodeList but could not retrieve the index for it's position in the list", content.Id)); + { + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); + number += 1; // legacy is zero-based + return content.FollowingSibling(number); } - public static IPublishedContent Sibling(this IPublishedContent content, string nodeTypeAlias) - { - var siblings = content.Siblings(); - var container = siblings.ToList(); - var currentIndex = container.FindIndex(n => n.Id == content.Id); - if (currentIndex != -1) - { - var workingIndex = currentIndex + 1; - while (workingIndex != currentIndex) - { - var working = container.ElementAtOrDefault(workingIndex); - if (working != null && working.DocumentTypeAlias == nodeTypeAlias) - { - return working; - } - workingIndex++; - if (workingIndex > container.Count) - { - workingIndex = 0; - } - } - return null; - } - throw new IndexOutOfRangeException(string.Format("Node {0} belongs to a DynamicNodeList but could not retrieve the index for it's position in the list", content.Id)); - } - - /// - /// Return the items siblings - /// - /// - /// + // contentTypeAlias is case-insensitive + [Obsolete("Obsolete, use FollowingSibling or PrecedingSibling instead.")] + public static IPublishedContent Sibling(this IPublishedContent content, string contentTypeAlias) + { + // note: the original implementation seems to loop on all siblings + // ie if it reaches the end of the set, it starts again at the beginning. + // so here we wrap, although it's not consistent... but anyway those + // methods should be obsoleted. + + return content.FollowingSibling(contentTypeAlias, true); + } + + // following-sibling, preceding-sibling axes + + public static IPublishedContent FollowingSibling(this IPublishedContent content) + { + return content.Siblings().ElementAtOrDefault(content.GetIndex(content.Siblings()) + 1); + } + + public static IPublishedContent FollowingSibling(this IPublishedContent content, int number) + { + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); + return number == 0 ? content : content.Siblings().ElementAtOrDefault(content.GetIndex(content.Siblings()) + number); + } + + // contentTypeAlias is case-insensitive + public static IPublishedContent FollowingSibling(this IPublishedContent content, string contentTypeAlias) + { + return content.FollowingSibling(contentTypeAlias, false); + } + + // contentTypeAlias is case-insensitive + // note: not sure that one makes a lot of sense but it is here for backward compatibility + public static IPublishedContent FollowingSibling(this IPublishedContent content, string contentTypeAlias, bool wrap) + { + return content.Next(content.Siblings(), x => x.DocumentTypeAlias.InvariantEquals(contentTypeAlias), wrap); + } + + public static T FollowingSibling(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.FollowingSibling(false); + } + + public static T FollowingSibling(this IPublishedContent content, bool wrap) + where T : class, IPublishedContent + { + return content.Next(content.Siblings(), x => x is T, wrap) as T; + } + + public static IPublishedContent PrecedingSibling(this IPublishedContent content) + { + return content.Siblings().ElementAtOrDefault(content.GetIndex(content.Siblings()) - 1); + } + + public static IPublishedContent PrecedingSibling(this IPublishedContent content, int number) + { + if (number < 0) + throw new ArgumentOutOfRangeException("number", "Must be greater than, or equal to, zero."); + return number == 0 ? content : content.Siblings().ElementAtOrDefault(content.GetIndex(content.Siblings()) - number); + } + + // contentTypeAlias is case-insensitive + public static IPublishedContent PrecedingSibling(this IPublishedContent content, string contentTypeAlias) + { + return content.PrecedingSibling(contentTypeAlias, false); + } + + // contentTypeAlias is case-insensitive + // note: not sure that one makes a lot of sense but it is here for backward compatibility + public static IPublishedContent PrecedingSibling(this IPublishedContent content, string contentTypeAlias, bool wrap) + { + return content.Next(content.Siblings().Reverse(), x => x.DocumentTypeAlias.InvariantEquals(contentTypeAlias), wrap); + } + + public static T PrecedingSibling(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.PrecedingSibling(false); + } + + public static T PrecedingSibling(this IPublishedContent content, bool wrap) + where T : class, IPublishedContent + { + return content.Next(content.Siblings().Reverse(), x => x is T, wrap) as T; + } + + // following, preceding axes - NOT IMPLEMENTED + + // utilities + public static IEnumerable Siblings(this IPublishedContent content) { - //get the root docs if parent is null - return content.Parent == null + // content.Parent, content.Children and cache.GetAtRoot() should be fast enough, + // or cached by the content cache, so that we don't have to implement cache here. + + // returns the true tree siblings, even if the content is in a set + // get the root docs if parent is null + + // note: I don't like having to refer to the "current" content cache here, but + // what else? would need root content to have a special, non-null but hidden, + // parent... + + var siblings = content.Parent == null ? UmbracoContext.Current.ContentCache.GetAtRoot() : content.Parent.Children; - } + + // make sure we run it once + return siblings.ToArray(); + } + + public static IEnumerable Siblings(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Siblings().OfType(); + } #endregion - /// - /// Method to return the Children of the content item + #region Axes: parent + + // Parent is native + + /// + /// Gets the parent of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The parent of content, of the given content type, else null. + public static T Parent(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Parent as T; + } + + #endregion + + #region Axes: children + + /// + /// Gets the children of the content. /// - /// - /// + /// The content. + /// The children of the content. /// - /// This method exists for consistency, it is the same as calling content.Children as a property. + /// Children are sorted by their sortOrder. + /// This method exists for consistency, it is the same as calling content.Children as a property. /// - public static IEnumerable Children(this IPublishedContent p) + public static IEnumerable Children(this IPublishedContent content) { - return p.Children.OrderBy(x => x.SortOrder); + return content.Children; + } + + /// + /// Gets the children of the content, filtered by a predicate. + /// + /// The content. + /// The predicate. + /// The children of the content, filtered by the predicate. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable Children(this IPublishedContent content, Func predicate) + { + return content.Children().Where(predicate); + } + + /// + /// Gets the children of the content, of a given content type. + /// + /// The content type. + /// The content. + /// The children of content, of the given content type. + /// + /// Children are sorted by their sortOrder. + /// + public static IEnumerable Children(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.Children().OfType(); + } + + /// + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// An optional content type alias. + /// The children of the content. + public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "") + { + return GenerateDataTable(content, contentTypeAliasFilter); } /// - /// Returns a DataTable object for the IPublishedContent - /// - /// - /// - /// - public static DataTable ChildrenAsTable(this IPublishedContent d, string nodeTypeAliasFilter = "") + /// Gets the children of the content in a DataTable. + /// + /// The content. + /// An optional content type alias. + /// The children of the content. + private static DataTable GenerateDataTable(IPublishedContent content, string contentTypeAliasFilter = "") { - return GenerateDataTable(d, nodeTypeAliasFilter); - } - - /// - /// Generates the DataTable for the IPublishedContent - /// - /// - /// - /// - private static DataTable GenerateDataTable(IPublishedContent node, string nodeTypeAliasFilter = "") - { - var firstNode = nodeTypeAliasFilter.IsNullOrWhiteSpace() - ? node.Children.Any() - ? node.Children.ElementAt(0) + var firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() + ? content.Children.Any() + ? content.Children.ElementAt(0) : null - : node.Children.FirstOrDefault(x => x.DocumentTypeAlias == nodeTypeAliasFilter); + : content.Children.FirstOrDefault(x => x.DocumentTypeAlias == contentTypeAliasFilter); if (firstNode == null) return new DataTable(); //no children found - var urlProvider = UmbracoContext.Current.RoutingContext.UrlProvider; - //use new utility class to create table so that we don't have to maintain code in many places, just one - var dt = Umbraco.Core.DataTableExtensions.GenerateDataTable( + var dt = Core.DataTableExtensions.GenerateDataTable( //pass in the alias of the first child node since this is the node type we're rendering headers for firstNode.DocumentTypeAlias, //pass in the callback to extract the Dictionary of all defined aliases to their names @@ -1164,34 +1596,36 @@ namespace Umbraco.Web () => { //create all row data - var tableData = Umbraco.Core.DataTableExtensions.CreateTableData(); + var tableData = Core.DataTableExtensions.CreateTableData(); //loop through each child and create row data for it - foreach (var n in node.Children.OrderBy(x => x.SortOrder)) + foreach (var n in content.Children.OrderBy(x => x.SortOrder)) { - if (!nodeTypeAliasFilter.IsNullOrWhiteSpace()) + if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) { - if (n.DocumentTypeAlias != nodeTypeAliasFilter) + if (n.DocumentTypeAlias != contentTypeAliasFilter) continue; //skip this one, it doesn't match the filter } - var standardVals = new Dictionary() - { - {"Id", n.Id}, - {"NodeName", n.Name}, - {"NodeTypeAlias", n.DocumentTypeAlias}, - {"CreateDate", n.CreateDate}, - {"UpdateDate", n.UpdateDate}, - {"CreatorName", n.CreatorName}, - {"WriterName", n.WriterName}, - {"Url", urlProvider.GetUrl(n.Id)} + var standardVals = new Dictionary + { + { "Id", n.Id }, + { "NodeName", n.Name }, + { "NodeTypeAlias", n.DocumentTypeAlias }, + { "CreateDate", n.CreateDate }, + { "UpdateDate", n.UpdateDate }, + { "CreatorName", n.CreatorName }, + { "WriterName", n.WriterName }, + { "Url", n.Url } }; + var userVals = new Dictionary(); - foreach (var p in from IPublishedContentProperty p in n.Properties where p.Value != null select p) - { - userVals[p.Alias] = p.Value; + foreach (var p in from IPublishedProperty p in n.Properties where p.DataValue != null select p) + { + // probably want the "object value" of the property here... + userVals[p.PropertyTypeAlias] = p.ObjectValue; } //add the row data - Umbraco.Core.DataTableExtensions.AddRowData(tableData, standardVals, userVals); + Core.DataTableExtensions.AddRowData(tableData, standardVals, userVals); } return tableData; } @@ -1199,7 +1633,29 @@ namespace Umbraco.Web return dt; } - private static Func> _getPropertyAliasesAndNames; + #endregion + + #region OfTypes + + // the .OfType() filter is nice when there's only one type + // this is to support filtering with multiple types + + public static IEnumerable OfTypes(this IEnumerable contents, params Type[] types) + { + return contents.Where(x => types.Contains(x.GetType())); + } + + public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) + { + types = types.Select(x => x.ToLowerInvariant()).ToArray(); + return contents.Where(x => types.Contains(x.DocumentTypeAlias.ToLowerInvariant())); + } + + #endregion + + #region PropertyAliasesAndNames + + private static Func> _getPropertyAliasesAndNames; /// /// This is used only for unit tests to set the delegate to look up aliases/names dictionary of a content type @@ -1223,7 +1679,7 @@ namespace Umbraco.Web {"WriterName", "WriterName"}, {"Url", "Url"} }; - foreach (var f in userFields.Where(f => !allFields.ContainsKey(f.Key))) + foreach (var f in userFields.Where(f => allFields.ContainsKey(f.Key) == false)) { allFields.Add(f.Key, f.Value); } @@ -1231,6 +1687,8 @@ namespace Umbraco.Web }); } set { _getPropertyAliasesAndNames = value; } - } - } + } + + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedContentPropertyExtension.cs b/src/Umbraco.Web/PublishedContentPropertyExtension.cs new file mode 100644 index 0000000000..efcf4d6fb6 --- /dev/null +++ b/src/Umbraco.Web/PublishedContentPropertyExtension.cs @@ -0,0 +1,61 @@ +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web +{ + /// + /// Provides extension methods for IPublishedProperty. + /// + public static class PublishedPropertyExtension + { + #region GetValue + + public static T GetValue(this IPublishedProperty property) + { + return property.GetValue(false, default(T)); + } + + public static T GetValue(this IPublishedProperty property, T defaultValue) + { + return property.GetValue(true, defaultValue); + } + + internal static T GetValue(this IPublishedProperty property, bool withDefaultValue, T defaultValue) + { + if (property.HasValue == false && withDefaultValue) return defaultValue; + + // else we use .Value so we give the converter a chance to handle the default value differently + // eg for IEnumerable it may return Enumerable.Empty instead of null + + var value = property.ObjectValue; + + // if value is null (strange but why not) it still is OK to call TryConvertTo + // because it's an extension method (hence no NullRef) which will return a + // failed attempt. So, no need to care for value being null here. + + // if already the requested type, return + if (value is T) return (T)value; + + // if can convert to requested type, return + var convert = value.TryConvertTo(); + if (convert.Success) return convert.Result; + + // at that point, the code tried with the raw value + // that makes no sense because it sort of is unpredictable, + // you wouldn't know when the converters run or don't run. + // so, it's commented out now. + + // try with the raw value + //var source = property.ValueSource; + //if (source is string) source = TextValueConverterHelper.ParseStringValueSource((string)source); + //if (source is T) return (T)source; + //convert = source.TryConvertTo(); + //if (convert.Success) return convert.Result; + + return defaultValue; + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 114cb0de28..cfd935a741 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -15,12 +15,26 @@ namespace Umbraco.Web.Templates /// public static class TemplateUtilities { - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - public static string ParseInternalLinks(string text) + internal static string ParseInternalLinks(string text, bool preview) + { + // save and set for url provider + var inPreviewMode = UmbracoContext.Current.InPreviewMode; + UmbracoContext.Current.InPreviewMode = preview; + + text = ParseInternalLinks(text); + + // restore + UmbracoContext.Current.InPreviewMode = inPreviewMode; + + return text; + } + + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + public static string ParseInternalLinks(string text) { //don't attempt to proceed without a context as we cannot lookup urls without one if (UmbracoContext.Current == null || UmbracoContext.Current.RoutingContext == null) @@ -31,57 +45,59 @@ namespace Umbraco.Web.Templates var urlProvider = UmbracoContext.Current.UrlProvider; // Parse internal links - MatchCollection tags = Regex.Matches(text, @"href=""[/]?(?:\{|\%7B)localLink:([0-9]+)(?:\}|\%7D)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + var tags = Regex.Matches(text, @"href=""[/]?(?:\{|\%7B)localLink:([0-9]+)(?:\}|\%7D)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); foreach (Match tag in tags) if (tag.Groups.Count > 0) { - string id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - string newLink = urlProvider.GetUrl(int.Parse(id)); - text = text.Replace(tag.Value.ToString(), "href=\"" + newLink); + var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); + var newLink = urlProvider.GetUrl(int.Parse(id)); + text = text.Replace(tag.Value, "href=\"" + newLink); } - return text; + + return text; } // static compiled regex for faster performance private readonly static Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - /// - /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. - /// - /// - /// - /// - /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. - /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. - /// - public static string ResolveUrlsFromTextString(string text) - { - if (UmbracoConfiguration.Current.UmbracoSettings.Content.ResolveUrlsFromTextString) - { - using (var timer = DisposableTimer.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) - { - // find all relative urls (ie. urls that contain ~) - var tags = ResolveUrlPattern.Matches(text); - LogHelper.Debug(typeof(IOHelper), "After regex: " + timer.Stopwatch.ElapsedMilliseconds + " matched: " + tags.Count); - foreach (Match tag in tags) - { - string url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; - // The richtext editor inserts a slash in front of the url. That's why we need this little fix - // if (url.StartsWith("/")) - // text = text.Replace(url, ResolveUrl(url.Substring(1))); - // else - if (!String.IsNullOrEmpty(url)) - { - string resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); - text = text.Replace(url, resolvedUrl); - } - } - } - } - return text; + /// + /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. + /// + /// + /// + /// + /// + /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. + /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. + /// + public static string ResolveUrlsFromTextString(string text) + { + if (UmbracoConfiguration.Current.UmbracoSettings.Content.ResolveUrlsFromTextString == false) return text; + + using (var timer = DisposableTimer.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) + { + // find all relative urls (ie. urls that contain ~) + var tags = ResolveUrlPattern.Matches(text); + LogHelper.Debug(typeof(IOHelper), "After regex: " + timer.Stopwatch.ElapsedMilliseconds + " matched: " + tags.Count); + foreach (Match tag in tags) + { + var url = ""; + if (tag.Groups[1].Success) + url = tag.Groups[1].Value; + + // The richtext editor inserts a slash in front of the url. That's why we need this little fix + // if (url.StartsWith("/")) + // text = text.Replace(url, ResolveUrl(url.Substring(1))); + // else + if (String.IsNullOrEmpty(url) == false) + { + var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); + text = text.Replace(url, resolvedUrl); + } + } + } + + return text; } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ec8dec8fdf..9308ee76f4 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -366,6 +366,7 @@ + @@ -491,7 +492,6 @@ - @@ -519,6 +519,7 @@ + @@ -545,6 +546,7 @@ AssignDomain2.aspx + ASPXCodeBehind @@ -616,7 +618,7 @@ - + @@ -786,8 +788,8 @@ - - + + @@ -1967,7 +1969,9 @@ ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index a0d2dbf39d..f306ac6806 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Web; using Umbraco.Core; using Umbraco.Core.Services; @@ -32,7 +33,6 @@ namespace Umbraco.Web private static readonly object Locker = new object(); private bool _replacing; - private PreviewContent _previewContent; /// /// Used if not running in a web application (no real HttpContext) @@ -407,7 +407,8 @@ namespace Umbraco.Web /// /// Determines whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) /// - public bool InPreviewMode { get; private set; } + /// Can be internally set by the RTE macro rendering to render macros in the appropriate mode. + public bool InPreviewMode { get; internal set; } private bool DetectInPreviewModeFromRequest() { @@ -441,7 +442,6 @@ namespace Umbraco.Web { Security.DisposeIfDisposable(); Security = null; - _previewContent = null; _umbracoContext = null; //ensure not to dispose this! Application = null; diff --git a/src/Umbraco.Web/UmbracoContextExtensions.cs b/src/Umbraco.Web/UmbracoContextExtensions.cs new file mode 100644 index 0000000000..cba1193cdb --- /dev/null +++ b/src/Umbraco.Web/UmbracoContextExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Web +{ + /// + /// Provides extension methods for . + /// + public static class UmbracoContextExtensions + { + /// + /// Informs the context that content has changed. + /// + /// The context. + /// + /// The contextual caches may, although that is not mandatory, provide an immutable snapshot of + /// the content over the duration of the context. If you make changes to the content and do want to have + /// the caches update their snapshot, you have to explicitely ask them to do so by calling ContentHasChanged. + /// The context informs the contextual caches that content has changed. + /// + public static void ContentHasChanged(this UmbracoContext context) + { + context.ContentCache.ContentHasChanged(); + context.MediaCache.ContentHasChanged(); + } + } +} diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index c0b59819c4..b381a50326 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -435,10 +435,19 @@ namespace Umbraco.Web /// String with a friendly url from a node public string NiceUrl(int nodeId) { - var urlProvider = UmbracoContext.Current.UrlProvider; - return urlProvider.GetUrl(nodeId); + return Url(nodeId); } + /// + /// Gets the url of a content identified by its identifier. + /// + /// The content identifier. + /// The url for the content. + public string Url(int contentId) + { + return UmbracoContext.Current.UrlProvider.GetUrl(contentId); + } + /// /// This method will always add the domain to the path if the hostnames are set up correctly. /// @@ -446,10 +455,19 @@ namespace Umbraco.Web /// String with a friendly url with full domain from a node public string NiceUrlWithDomain(int nodeId) { - var urlProvider = UmbracoContext.Current.UrlProvider; - return urlProvider.GetUrl(nodeId, true); + return UrlAbsolute(nodeId); } + /// + /// Gets the absolute url of a content identified by its identifier. + /// + /// The content identifier. + /// The absolute url for the content. + public string UrlAbsolute(int contentId) + { + return UmbracoContext.Current.UrlProvider.GetUrl(contentId, true); + } + #endregion #region Content @@ -521,27 +539,27 @@ namespace Umbraco.Web public dynamic Content(object id) { - return DocumentById(id, _umbracoContext.ContentCache, new DynamicNull()); + return DocumentById(id, _umbracoContext.ContentCache, DynamicNull.Null); } public dynamic Content(int id) { - return DocumentById(id, _umbracoContext.ContentCache, new DynamicNull()); + return DocumentById(id, _umbracoContext.ContentCache, DynamicNull.Null); } public dynamic Content(string id) { - return DocumentById(id, _umbracoContext.ContentCache, new DynamicNull()); + return DocumentById(id, _umbracoContext.ContentCache, DynamicNull.Null); } public dynamic ContentSingleAtXPath(string xpath, params XPathVariable[] vars) { - return DocumentByXPath(xpath, vars, _umbracoContext.ContentCache, new DynamicNull()); + return DocumentByXPath(xpath, vars, _umbracoContext.ContentCache, DynamicNull.Null); } public dynamic ContentSingleAtXPath(XPathExpression xpath, params XPathVariable[] vars) { - return DocumentByXPath(xpath, vars, _umbracoContext.ContentCache, new DynamicNull()); + return DocumentByXPath(xpath, vars, _umbracoContext.ContentCache, DynamicNull.Null); } public dynamic Content(params object[] ids) @@ -655,17 +673,17 @@ namespace Umbraco.Web public dynamic Media(object id) { - return DocumentById(id, _umbracoContext.MediaCache, new DynamicNull()); + return DocumentById(id, _umbracoContext.MediaCache, DynamicNull.Null); } public dynamic Media(int id) { - return DocumentById(id, _umbracoContext.MediaCache, new DynamicNull()); + return DocumentById(id, _umbracoContext.MediaCache, DynamicNull.Null); } public dynamic Media(string id) { - return DocumentById(id, _umbracoContext.MediaCache, new DynamicNull()); + return DocumentById(id, _umbracoContext.MediaCache, DynamicNull.Null); } public dynamic Media(params object[] ids) @@ -862,7 +880,7 @@ namespace Umbraco.Web /// private dynamic DocumentByIds(ContextualPublishedCache cache, params object[] ids) { - var dNull = new DynamicNull(); + var dNull = DynamicNull.Null; var nodes = ids.Select(eachId => DocumentById(eachId, cache, dNull)) .Where(x => !TypeHelper.IsTypeAssignableFrom(x)) .Cast(); @@ -871,7 +889,7 @@ namespace Umbraco.Web private dynamic DocumentByIds(ContextualPublishedCache cache, params int[] ids) { - var dNull = new DynamicNull(); + var dNull = DynamicNull.Null; var nodes = ids.Select(eachId => DocumentById(eachId, cache, dNull)) .Where(x => !TypeHelper.IsTypeAssignableFrom(x)) .Cast(); @@ -880,7 +898,7 @@ namespace Umbraco.Web private dynamic DocumentByIds(ContextualPublishedCache cache, params string[] ids) { - var dNull = new DynamicNull(); + var dNull = DynamicNull.Null; var nodes = ids.Select(eachId => DocumentById(eachId, cache, dNull)) .Where(x => !TypeHelper.IsTypeAssignableFrom(x)) .Cast(); diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index e3ef5d5e04..b4ca1ddeda 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Macros; using Umbraco.Core.ObjectResolution; using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Sync; using Umbraco.Web.Dictionary; using Umbraco.Web.Macros; @@ -21,6 +22,8 @@ using Umbraco.Web.Media; using Umbraco.Web.Media.ThumbnailProviders; using Umbraco.Web.Models; using Umbraco.Web.Mvc; +using Umbraco.Web.PropertyEditors; +using Umbraco.Web.PropertyEditors.ValueConverters; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -281,10 +284,12 @@ namespace Umbraco.Web UmbracoApiControllerResolver.Current = new UmbracoApiControllerResolver( PluginManager.Current.ResolveUmbracoApiControllers()); - //the base creates the PropertyEditorValueConvertersResolver but we want to modify it in the web app and remove - //the TinyMcePropertyEditorValueConverter since when the web app is loaded the RteMacroRenderingPropertyEditorValueConverter - //is found and we'll use that instead. - PropertyValueConvertersResolver.Current.RemoveType(); + // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be + // discovered when CoreBootManager configures the converters. We HAVE to remove one of them + // here because there cannot be two converters for one property editor - and we want the full + // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. + // (why it exists in in the first place, I'm not sure to understand) + PropertyValueConvertersResolver.Current.RemoveType(); PublishedCachesResolver.Current = new PublishedCachesResolver(new PublishedCaches( new PublishedCache.XmlPublishedCache.PublishedContentCache(), diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 168abe3213..1b960c6b9f 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -6,6 +6,7 @@ using StackExchange.Profiling; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models; +using Umbraco.Web; using Umbraco.Core.Profiling; namespace umbraco @@ -69,8 +70,9 @@ namespace umbraco } else { - var recursiveVal = publishedContent.GetRecursiveValue(_fieldName); - _fieldContent = recursiveVal.IsNullOrWhiteSpace() ? _fieldContent : recursiveVal; + var pval = publishedContent.GetPropertyValue(_fieldName, true); + var rval = pval == null ? string.Empty : pval.ToString(); + _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } } else diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index abfe246202..b8a54ed1b2 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -356,9 +356,16 @@ namespace umbraco return doc.CreatorName; } + // in 4.9.0 the method returned the raw XML from the cache, unparsed + // starting with 5c20f4f (4.10?) the method returns prop.Value.ToString() + // where prop.Value is parsed for internal links + resolve urls - but not for macros + // comments say "fixing U4-917 and U4-821" which are not related + // if we return DataValue.ToString() we're back to the original situation + // if we return ObjectValue.ToString() we'll have macros parsed and that's nice + // + // so, use ObjectValue.ToString() here. var prop = doc.GetProperty(alias); - return prop == null ? string.Empty : prop.Value.ToString(); - + return prop == null ? string.Empty : prop.ObjectValue.ToString(); } /// diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 3653098d07..a029d61474 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -22,6 +22,7 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Macros; +using Umbraco.Core.Xml.XPath; using Umbraco.Core.Profiling; using Umbraco.Web; using Umbraco.Web.Cache; @@ -49,6 +50,7 @@ namespace umbraco /// Cache for . private static Dictionary _predefinedExtensions; + private static XsltSettings _xsltSettings; private const string LoadUserControlKey = "loadUserControl"; private readonly StringBuilder _content = new StringBuilder(); private const string MacrosAddedKey = "macrosAdded"; @@ -59,6 +61,13 @@ namespace umbraco get { return Application.SqlHelper; } } + static macro() + { + _xsltSettings = GlobalSettings.ApplicationTrustLevel > AspNetHostingPermissionLevel.Medium + ? XsltSettings.TrustedXslt + : XsltSettings.Default; + } + #endregion #region public properties @@ -814,14 +823,7 @@ namespace umbraco try { - if (GlobalSettings.ApplicationTrustLevel > AspNetHostingPermissionLevel.Medium) - { - macroXslt.Load(xslReader, XsltSettings.TrustedXslt, xslResolver); - } - else - { - macroXslt.Load(xslReader, XsltSettings.Default, xslResolver); - } + macroXslt.Load(xslReader, _xsltSettings, xslResolver); } finally { @@ -852,21 +854,41 @@ namespace umbraco using (DisposableTimer.DebugDuration("Executing XSLT: " + XsltFile)) { XmlDocument macroXml = null; + MacroNavigator macroNavigator = null; + NavigableNavigator contentNavigator = null; - // get master xml document - var cache = UmbracoContext.Current.ContentCache.InnerCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; - if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); - XmlDocument umbracoXml = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); - macroXml = new XmlDocument(); - macroXml.LoadXml(""); - foreach (var prop in macro.Model.Properties) + var canNavigate = + UmbracoContext.Current.ContentCache.XPathNavigatorIsNavigable && + UmbracoContext.Current.MediaCache.XPathNavigatorIsNavigable; + + if (!canNavigate) { - AddMacroXmlNode(umbracoXml, macroXml, prop.Key, prop.Type, prop.Value); + // get master xml document + var cache = UmbracoContext.Current.ContentCache.InnerCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; + if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); + XmlDocument umbracoXml = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); + macroXml = new XmlDocument(); + macroXml.LoadXml(""); + foreach (var prop in macro.Model.Properties) + { + AddMacroXmlNode(umbracoXml, macroXml, prop.Key, prop.Type, prop.Value); + } + } + else + { + var parameters = new List(); + contentNavigator = UmbracoContext.Current.ContentCache.GetXPathNavigator() as NavigableNavigator; + var mediaNavigator = UmbracoContext.Current.MediaCache.GetXPathNavigator() as NavigableNavigator; + foreach (var prop in macro.Model.Properties) + { + AddMacroParameter(parameters, contentNavigator, mediaNavigator, prop.Key, prop.Type, prop.Value); + } + macroNavigator = new MacroNavigator(parameters); } if (HttpContext.Current.Request.QueryString["umbDebug"] != null && GlobalSettings.DebugMode) { - var outerXml = macroXml.OuterXml; + var outerXml = macroXml == null ? macroNavigator.OuterXml : macroXml.OuterXml; return new LiteralControl("
Debug from " + macro.Name + @@ -882,7 +904,9 @@ namespace umbraco { try { - var transformed = GetXsltTransformResult(macroXml, xsltFile); + var transformed = canNavigate + ? GetXsltTransformResult(macroNavigator, contentNavigator, xsltFile) // better? + : GetXsltTransformResult(macroXml, xsltFile); // document var result = CreateControlsFromText(transformed); return result; @@ -1201,6 +1225,101 @@ namespace umbraco macroXml.FirstChild.AppendChild(macroXmlNode); } + // add parameters to the macro parameters collection + private void AddMacroParameter(ICollection parameters, + NavigableNavigator contentNavigator, NavigableNavigator mediaNavigator, + string macroPropertyAlias,string macroPropertyType, string macroPropertyValue) + { + // if no value is passed, then use the current "pageID" as value + var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; + + TraceInfo("umbracoMacro", + "Xslt node adding search start (" + macroPropertyAlias + ",'" + + macroPropertyValue + "')"); + + // beware! do not use the raw content- or media- navigators, but clones !! + + switch (macroPropertyType) + { + case "contentTree": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + attributes: new Dictionary { { "nodeID", contentId } })); + + break; + + case "contentPicker": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 0)); + break; + + case "contentSubs": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + contentNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 1)); + break; + + case "contentAll": + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, contentNavigator.Clone())); + break; + + case "contentRandom": + var nav = contentNavigator.Clone(); + if (nav.MoveToId(contentId)) + { + var descendantIterator = nav.Select("./* [@isDoc]"); + if (descendantIterator.MoveNext()) + { + // not empty - and won't change + var descendantCount = descendantIterator.Count; + + int index; + var r = library.GetRandom(); + lock (r) + { + index = r.Next(descendantCount); + } + + while (index > 0 && descendantIterator.MoveNext()) + index--; + + var node = descendantIterator.Current.UnderlyingObject as INavigableContent; + if (node != null) + { + nav = contentNavigator.CloneWithNewRoot(node.Id.ToString(CultureInfo.InvariantCulture)); + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, nav, 0)); + } + else + throw new InvalidOperationException("Iterator contains non-INavigableContent elements."); + } + else + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't have children!"); + } + else + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't exists!"); + break; + + case "mediaCurrent": + parameters.Add(new MacroNavigator.MacroParameter( + macroPropertyAlias, + mediaNavigator.CloneWithNewRoot(contentId), // null if not found - will be reported as empty + 0)); + break; + + default: + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, HttpContext.Current.Server.HtmlDecode(macroPropertyValue))); + break; + } + } + #endregion /// diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index 58fc1a4d94..b63317c577 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -273,9 +273,9 @@ namespace umbraco { foreach(var p in node.Properties) { - if (!_elements.ContainsKey(p.Alias)) + if (!_elements.ContainsKey(p.PropertyTypeAlias)) { - _elements[p.Alias] = p.Value; + _elements[p.PropertyTypeAlias] = p.ObjectValue; } } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 38dff3dfff..9904e9283d 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -96,6 +96,8 @@ namespace umbraco.presentation.preview //Inject preview xml parentId = document.Level == 1 ? -1 : document.Parent.Id; var previewXml = document.ToPreviewXml(XmlContent); + if (document.HasPendingChanges()) // HasPendingChanges is obsolete but what's the equivalent that wouldn't hit the DB? + previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); content.AppendDocumentXml(document.Id, document.Level, parentId, previewXml, XmlContent); } @@ -103,7 +105,10 @@ namespace umbraco.presentation.preview { foreach (var prevNode in documentObject.GetNodesForPreview(true)) { - XmlContent = content.AppendDocumentXml(prevNode.NodeId, prevNode.Level, prevNode.ParentId, XmlContent.ReadNode(XmlReader.Create(new StringReader(prevNode.Xml))), XmlContent); + var previewXml = XmlContent.ReadNode(XmlReader.Create(new StringReader(prevNode.Xml))); + if (prevNode.IsDraft) + previewXml.Attributes.Append(XmlContent.CreateAttribute("isDraft")); + XmlContent = content.AppendDocumentXml(prevNode.NodeId, prevNode.Level, prevNode.ParentId, previewXml, XmlContent); } } diff --git a/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNull.cs b/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNull.cs index 5628a755ff..c06e6ae8b7 100644 --- a/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNull.cs +++ b/src/umbraco.MacroEngines/RazorDynamicNode/DynamicNull.cs @@ -16,7 +16,7 @@ namespace umbraco.MacroEngines [Obsolete("This class has been superceded by Umbraco.Core.Dynamics.DynamicNull")] public class DynamicNull : DynamicObject, IEnumerable, IHtmlString { - private readonly Umbraco.Core.Dynamics.DynamicNull _inner = new Umbraco.Core.Dynamics.DynamicNull(); + private readonly Umbraco.Core.Dynamics.DynamicNull _inner = Umbraco.Core.Dynamics.DynamicNull.Null; public IEnumerator GetEnumerator() { diff --git a/src/umbraco.MacroEngines/RazorDynamicNode/PropertyResult.cs b/src/umbraco.MacroEngines/RazorDynamicNode/PropertyResult.cs index 6d9696e836..f82407d53b 100644 --- a/src/umbraco.MacroEngines/RazorDynamicNode/PropertyResult.cs +++ b/src/umbraco.MacroEngines/RazorDynamicNode/PropertyResult.cs @@ -10,27 +10,29 @@ namespace umbraco.MacroEngines { public class PropertyResult : IProperty, IHtmlString { - private string _alias; - private string _value; + private readonly string _alias; + private readonly string _value; public PropertyResult(IProperty source) { - if (source != null) - { - this._alias = source.Alias; - this._value = source.Value; - } + if (source == null) return; + + _alias = source.Alias; + _value = source.Value; } + public PropertyResult(string alias, string value) { - this._alias = alias; - this._value = value; + _alias = alias; + _value = value; } + public PropertyResult(Property source) { - this._alias = source.PropertyType.Alias; - this._value = string.Format("{0}", source.Value); + _alias = source.PropertyType.Alias; + _value = source.Value.ToString(); } + public string Alias { get { return _alias; } @@ -45,6 +47,7 @@ namespace umbraco.MacroEngines { return Value == null; } + public bool HasValue() { return !string.IsNullOrWhiteSpace(Value); @@ -53,9 +56,9 @@ namespace umbraco.MacroEngines public int ContextId { get; set; } public string ContextAlias { get; set; } + // implements IHtmlString.ToHtmlString public string ToHtmlString() { - //Like a boss return Value; } } diff --git a/src/umbraco.MacroEngines/RazorDynamicNode/PublishedContentExtensions.cs b/src/umbraco.MacroEngines/RazorDynamicNode/PublishedContentExtensions.cs index 1b52810943..05f419d2e8 100644 --- a/src/umbraco.MacroEngines/RazorDynamicNode/PublishedContentExtensions.cs +++ b/src/umbraco.MacroEngines/RazorDynamicNode/PublishedContentExtensions.cs @@ -12,15 +12,15 @@ using Property = umbraco.NodeFactory.Property; namespace umbraco.MacroEngines.Library { - /// - /// Extension methods for converting DynamicPublishedContent to INode - /// + /// + /// Provides extension methods for IPublishedContent. + /// + /// These are dedicated to converting DynamicPublishedContent to INode. internal static class PublishedContentExtensions - { - - internal static IProperty ConvertToNodeProperty(this IPublishedContentProperty prop) + { + internal static IProperty ConvertToNodeProperty(this IPublishedProperty prop) { - return new PropertyResult(prop.Alias, prop.Value.ToString()); + return new PropertyResult(prop.PropertyTypeAlias, prop.ObjectValue.ToString()); } internal static INode ConvertToNode(this IPublishedContent doc) @@ -30,7 +30,7 @@ namespace umbraco.MacroEngines.Library } /// - /// Internal custom INode class used for conversions from DynamicPublishedContent + /// Internal custom INode class used for conversions from DynamicPublishedContent. /// private class ConvertedNode : INode { diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs index ba8a6ccc35..99f3ba337d 100644 --- a/src/umbraco.cms/businesslogic/CMSNode.cs +++ b/src/umbraco.cms/businesslogic/CMSNode.cs @@ -467,7 +467,8 @@ namespace umbraco.cms.businesslogic { List nodes = new List(); string sql = @" -select umbracoNode.id, umbracoNode.parentId, umbracoNode.level, umbracoNode.sortOrder, cmsPreviewXml.xml from umbracoNode +select umbracoNode.id, umbracoNode.parentId, umbracoNode.level, umbracoNode.sortOrder, cmsPreviewXml.xml +from umbracoNode inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id where trashed = 0 and path like '{0}' order by level,sortOrder"; @@ -476,7 +477,7 @@ order by level,sortOrder"; IRecordsReader dr = SqlHelper.ExecuteReader(String.Format(sql, pathExp)); while (dr.Read()) - nodes.Add(new CMSPreviewNode(dr.GetInt("id"), dr.GetGuid("uniqueID"), dr.GetInt("parentId"), dr.GetShort("level"), dr.GetInt("sortOrder"), dr.GetString("xml"))); + nodes.Add(new CMSPreviewNode(dr.GetInt("id"), dr.GetGuid("uniqueID"), dr.GetInt("parentId"), dr.GetShort("level"), dr.GetInt("sortOrder"), dr.GetString("xml"), false)); dr.Close(); return nodes; diff --git a/src/umbraco.cms/businesslogic/CMSPreviewNode.cs b/src/umbraco.cms/businesslogic/CMSPreviewNode.cs index fdcbeab082..5b3970821b 100644 --- a/src/umbraco.cms/businesslogic/CMSPreviewNode.cs +++ b/src/umbraco.cms/businesslogic/CMSPreviewNode.cs @@ -20,13 +20,14 @@ namespace umbraco.cms.businesslogic public int ParentId { get; set; } public int SortOrder { get; set; } public string Xml { get; set; } + public bool IsDraft { get; set; } public CMSPreviewNode() { } - public CMSPreviewNode(int nodeId, Guid version, int parentId, int level, int sortOrder, string xml) + public CMSPreviewNode(int nodeId, Guid version, int parentId, int level, int sortOrder, string xml, bool isDraft) { NodeId = nodeId; Version = version; @@ -34,6 +35,7 @@ namespace umbraco.cms.businesslogic Level = level; SortOrder = sortOrder; Xml = xml; + IsDraft = isDraft; } } } diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index d24a7dc439..fc74b70df6 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -133,7 +133,8 @@ namespace umbraco.cms.businesslogic.web #region Constants and Static members private const string SqlOptimizedForPreview = @" - select umbracoNode.id, umbracoNode.parentId, umbracoNode.level, umbracoNode.sortOrder, cmsDocument.versionId, cmsPreviewXml.xml from cmsDocument + select umbracoNode.id, umbracoNode.parentId, umbracoNode.level, umbracoNode.sortOrder, cmsDocument.versionId, cmsPreviewXml.xml, cmsDocument.published + from cmsDocument inner join umbracoNode on umbracoNode.id = cmsDocument.nodeId inner join cmsPreviewXml on cmsPreviewXml.nodeId = cmsDocument.nodeId and cmsPreviewXml.versionId = cmsDocument.versionId where newest = 1 and trashed = 0 and path like '{0}' @@ -1380,7 +1381,7 @@ namespace umbraco.cms.businesslogic.web IRecordsReader dr = SqlHelper.ExecuteReader(String.Format(SqlOptimizedForPreview, pathExp)); while (dr.Read()) - nodes.Add(new CMSPreviewNode(dr.GetInt("id"), dr.GetGuid("versionId"), dr.GetInt("parentId"), dr.GetShort("level"), dr.GetInt("sortOrder"), dr.GetString("xml"))); + nodes.Add(new CMSPreviewNode(dr.GetInt("id"), dr.GetGuid("versionId"), dr.GetInt("parentId"), dr.GetShort("level"), dr.GetInt("sortOrder"), dr.GetString("xml"), !dr.GetBoolean("published"))); dr.Close(); return nodes; diff --git a/src/umbraco.editorControls/datepicker/DateData.cs b/src/umbraco.editorControls/datepicker/DateData.cs index fe1edc7be4..d7e29bef8e 100644 --- a/src/umbraco.editorControls/datepicker/DateData.cs +++ b/src/umbraco.editorControls/datepicker/DateData.cs @@ -11,10 +11,17 @@ namespace umbraco.editorControls.datepicker public override System.Xml.XmlNode ToXMl(System.Xml.XmlDocument d) { - if (Value != null && Value.ToString() != "") - return d.CreateTextNode(((DateTime) Value).ToString("s")); - else - return d.CreateTextNode(""); + if (Value != null && Value.ToString() != "") + { + if(Value is DateTime) + return d.CreateTextNode(((DateTime) Value).ToString("s")); + + DateTime convertedDate; + if (DateTime.TryParse(Value.ToString(), out convertedDate)) + return d.CreateTextNode(convertedDate.ToString("s")); + } + + return d.CreateTextNode(""); } public override void MakeNew(int PropertyId)