Files
Umbraco-CMS/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs

418 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors.Validators;
using Umbraco.Core.Services;
namespace Umbraco.Core.PropertyEditors
{
/// <summary>
/// Represents a value editor.
/// </summary>
public class DataValueEditor : IDataValueEditor
{
private string _view;
/// <summary>
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
/// </summary>
public DataValueEditor() // for tests, and manifest
{
ValueType = ValueTypes.String;
Validators = new List<IValueValidator>();
}
/// <summary>
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
/// </summary>
public DataValueEditor(string view, params IValueValidator[] validators) // not used
: this()
{
View = view;
Validators.AddRange(validators);
}
/// <summary>
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
/// </summary>
public DataValueEditor(DataEditorAttribute attribute)
: this()
{
if (attribute == null) throw new ArgumentNullException(nameof(attribute));
var view = attribute.View;
if (string.IsNullOrWhiteSpace(view))
throw new ArgumentException("The attribute does not specify a view.", nameof(attribute));
View = view;
ValueType = attribute.ValueType;
HideLabel = attribute.HideLabel;
}
// fixme kabam!
// I don't understand the remarks in the code commented out below
// and then,
// IPropertyEditor come from a PropertyEditorCollection so they are singletons
// IValueEditor is the actual value editor used for editing the value,
// and it has its own configuration, depending on the datatype, so it
// should NOT be a singleton => do NOT cache it in PropertyEditor!
/// <summary>
/// Gets or sets the value editor configuration.
/// </summary>
public virtual object Configuration { get; set; }
//private PreValueCollection _preVals;
//protected PreValueCollection PreValues
//{
// get
// {
// if (_preVals == null)
// {
// throw new InvalidOperationException("Pre values cannot be accessed until the Configure method has been called");
// }
// return _preVals;
// }
//}
///// <summary>
///// This is called to configure the editor for display with it's prevalues, useful when properties need to change dynamically
///// depending on what is in the pre-values.
///// </summary>
///// <param name="preValues"></param>
///// <remarks>
///// This cannot be used to change the value being sent to the editor, ConfigureEditor will be called *after* ConvertDbToEditor, pre-values
///// should not be used to modify values.
///// </remarks>
//public virtual void ConfigureForDisplay(PreValueCollection preValues)
//{
// _preVals = preValues ?? throw new ArgumentNullException(nameof(preValues));
//}
/// <summary>
/// Gets or sets the editor view.
/// </summary>
/// <remarks>
/// <para>The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco
/// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html.</para>
/// </remarks>
[JsonProperty("view", Required = Required.Always)]
public string View
{
get => _view;
set => _view = IOHelper.ResolveVirtualUrl(value);
}
/// <summary>
/// The value type which reflects how it is validated and stored in the database
/// </summary>
[JsonProperty("valueType")]
public string ValueType { get; set; }
/// <inheritdoc />
public IEnumerable<ValidationResult> Validate(object value, bool required, string format)
{
List<ValidationResult> results = null;
var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList();
if (r.Any()) { results = r; }
// mandatory and regex validators cannot be part of valueEditor.Validators because they
// depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx,
// so they have to be explicitely invoked here.
if (required)
{
r = RequiredValidator.ValidateRequired(value, ValueType).ToList();
if (r.Any()) { if (results == null) results = r; else results.AddRange(r); }
}
var stringValue = value?.ToString();
if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue))
{
r = FormatValidator.ValidateFormat(value, ValueType, format).ToList();
if (r.Any()) { if (results == null) results = r; else results.AddRange(r); }
}
return results ?? Enumerable.Empty<ValidationResult>();
}
/// <summary>
/// A collection of validators for the pre value editor
/// </summary>
[JsonProperty("validation")]
public List<IValueValidator> Validators { get; private set; }
/// <summary>
/// Gets the validator used to validate the special property type -level "required".
/// </summary>
public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator();
/// <summary>
/// Gets the validator used to validate the special property type -level "format".
/// </summary>
public virtual IValueFormatValidator FormatValidator => new RegexValidator();
/// <summary>
/// If this is is true than the editor will be displayed full width without a label
/// </summary>
[JsonProperty("hideLabel")]
public bool HideLabel { get; set; }
/// <summary>
/// Set this to true if the property editor is for display purposes only
/// </summary>
public virtual bool IsReadOnly => false;
/// <summary>
/// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
internal Attempt<object> TryConvertValueToCrlType(object value)
{
if (value is JValue)
value = value.ToString();
//this is a custom check to avoid any errors, if it's a string and it's empty just make it null
if (value is string s && string.IsNullOrWhiteSpace(s))
value = null;
Type valueType;
//convert the string to a known type
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Ntext:
case ValueStorageType.Nvarchar:
valueType = typeof(string);
break;
case ValueStorageType.Integer:
//ensure these are nullable so we can return a null if required
//NOTE: This is allowing type of 'long' because I think json.net will deserialize a numerical value as long
// instead of int. Even though our db will not support this (will get truncated), we'll at least parse to this.
valueType = typeof(long?);
//if parsing is successful, we need to return as an Int, we're only dealing with long's here because of json.net, we actually
//don't support long values and if we return a long value it will get set as a 'long' on the Property.Value (object) and then
//when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match.
var result = value.TryConvertTo(valueType);
return result.Success && result.Result != null
? Attempt<object>.Succeed((int)(long)result.Result)
: result;
case ValueStorageType.Decimal:
//ensure these are nullable so we can return a null if required
valueType = typeof(decimal?);
break;
case ValueStorageType.Date:
//ensure these are nullable so we can return a null if required
valueType = typeof(DateTime?);
break;
default:
throw new ArgumentOutOfRangeException();
}
return value.TryConvertTo(valueType);
}
// fixme - not dealing with variants here!
//
// editors should declare whether they support variants, and then we should have a common
// way of dealing with it, ie of sending and receiving values, etc.
// eg
// [ { "value": "hello" }, { "lang": "fr-fr", "value": "bonjour" } ]
/// <summary>
/// A method to deserialize the string value that has been saved in the content editor
/// to an object to be stored in the database.
/// </summary>
/// <param name="editorValue"></param>
/// <param name="currentValue">
/// The current value that has been persisted to the database for this editor. This value may be usesful for
/// how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used.
/// </param>
/// <param name="languageId"></param>
/// <param name="segment"></param>
/// <returns></returns>
/// <remarks>
/// By default this will attempt to automatically convert the string value to the value type supplied by ValueType.
///
/// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the
/// value to the DB will fail when it tries to validate the value type.
/// </remarks>
public virtual object FromEditor(ContentPropertyData editorValue, object currentValue)
{
//if it's json but it's empty json, then return null
if (ValueType.InvariantEquals(ValueTypes.Json) && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson())
{
return null;
}
var result = TryConvertValueToCrlType(editorValue.Value);
if (result.Success == false)
{
Current.Logger.Warn<DataValueEditor>("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType));
return null;
}
return result.Result;
}
/// <summary>
/// A method used to format the database value to a value that can be used by the editor
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
/// <remarks>
/// The object returned will automatically be serialized into json notation. For most property editors
/// the value returned is probably just a string but in some cases a json structure will be returned.
/// </remarks>
public virtual object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null)
{
var val = property.GetValue(culture, segment);
if (val == null) return string.Empty;
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Ntext:
case ValueStorageType.Nvarchar:
//if it is a string type, we will attempt to see if it is json stored data, if it is we'll try to convert
//to a real json object so we can pass the true json object directly to angular!
var asString = val.ToString();
if (asString.DetectIsJson())
{
try
{
var json = JsonConvert.DeserializeObject(asString);
return json;
}
catch
{
//swallow this exception, we thought it was json but it really isn't so continue returning a string
}
}
return asString;
case ValueStorageType.Integer:
case ValueStorageType.Decimal:
//Decimals need to be formatted with invariant culture (dots, not commas)
//Anything else falls back to ToString()
var decim = val.TryConvertTo<decimal>();
return decim.Success
? decim.Result.ToString(NumberFormatInfo.InvariantInfo)
: val.ToString();
case ValueStorageType.Date:
var date = val.TryConvertTo<DateTime?>();
if (date.Success == false || date.Result == null)
{
return string.Empty;
}
//Dates will be formatted as yyyy-MM-dd HH:mm:ss
return date.Result.Value.ToIsoString();
default:
throw new ArgumentOutOfRangeException();
}
}
// fixme - the methods below should be replaced by proper property value convert ToXPath usage!
/// <summary>
/// Converts a property to Xml fragments.
/// </summary>
public IEnumerable<XElement> ConvertDbToXml(Property property, IDataTypeService dataTypeService, ILocalizationService localizationService, bool published)
{
published &= property.PropertyType.IsPublishing;
var nodeName = property.PropertyType.Alias.ToSafeAlias();
foreach (var pvalue in property.Values)
{
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
continue;
var xElement = new XElement(nodeName);
if (pvalue.Culture != null)
xElement.Add(new XAttribute("lang", pvalue.Culture));
if (pvalue.Segment != null)
xElement.Add(new XAttribute("segment", pvalue.Segment));
var xValue = ConvertDbToXml(property.PropertyType, value, dataTypeService);
xElement.Add(xValue);
yield return xElement;
}
}
/// <summary>
/// Converts a property value to an Xml fragment.
/// </summary>
/// <remarks>
/// <para>By default, this returns the value of ConvertDbToString but ensures that if the db value type is
/// NVarchar or NText, the value is returned as a CDATA fragment - elxe it's a Text fragment.</para>
/// <para>Returns an XText or XCData instance which must be wrapped in a element.</para>
/// <para>If the value is empty we will not return as CDATA since that will just take up more space in the file.</para>
/// </remarks>
public XNode ConvertDbToXml(PropertyType propertyType, object value, IDataTypeService dataTypeService)
{
//check for null or empty value, we don't want to return CDATA if that is the case
if (value == null || value.ToString().IsNullOrWhiteSpace())
{
return new XText(ConvertDbToString(propertyType, value, dataTypeService));
}
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Date:
case ValueStorageType.Integer:
case ValueStorageType.Decimal:
return new XText(ConvertDbToString(propertyType, value, dataTypeService));
case ValueStorageType.Nvarchar:
case ValueStorageType.Ntext:
//put text in cdata
return new XCData(ConvertDbToString(propertyType, value, dataTypeService));
default:
throw new ArgumentOutOfRangeException();
}
}
/// <summary>
/// Converts a property value to a string.
/// </summary>
public virtual string ConvertDbToString(PropertyType propertyType, object value, IDataTypeService dataTypeService)
{
if (value == null)
return string.Empty;
switch (ValueTypes.ToStorageType(ValueType))
{
case ValueStorageType.Nvarchar:
case ValueStorageType.Ntext:
return value.ToXmlString<string>();
case ValueStorageType.Integer:
case ValueStorageType.Decimal:
return value.ToXmlString(value.GetType());
case ValueStorageType.Date:
//treat dates differently, output the format as xml format
var date = value.TryConvertTo<DateTime?>();
if (date.Success == false || date.Result == null)
return string.Empty;
return date.Result.ToXmlString<DateTime>();
default:
throw new ArgumentOutOfRangeException();
}
}
}
}