Skip to content

Commit 9b4285c

Browse files
author
Claus
authored
Merge pull request #36 from umbraco/v3/feature/nestedcontent
adding nested content value connector.
2 parents 6081b0f + e53b983 commit 9b4285c

File tree

2 files changed

+274
-0
lines changed

2 files changed

+274
-0
lines changed

src/Umbraco.Deploy.Contrib.Connectors/Umbraco.Deploy.Contrib.Connectors.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,15 @@
161161
<ItemGroup>
162162
<Compile Include="Properties\AssemblyInfo.cs" />
163163
<Compile Include="Properties\VersionInfo.cs" />
164+
<Compile Include="ValueConnectors\NestedContentValueConnector.cs" />
164165
</ItemGroup>
165166
<ItemGroup>
166167
<Content Include="GridCellValueConnectors\dummy.txt" />
167168
</ItemGroup>
168169
<ItemGroup>
169170
<Folder Include="ValueConnectors\" />
170171
</ItemGroup>
172+
<ItemGroup />
171173
<ItemGroup>
172174
<None Include="packages.config" />
173175
</ItemGroup>
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Linq;
6+
using Umbraco.Core;
7+
using Umbraco.Core.Deploy;
8+
using Umbraco.Core.Logging;
9+
using Umbraco.Core.Models;
10+
using Umbraco.Core.Services;
11+
using Umbraco.Deploy.Connectors.ValueConnectors.Services;
12+
13+
namespace Umbraco.Deploy.Contrib.Connectors.ValueConnectors
14+
{
15+
/// <summary>
16+
/// A Deploy connector for the NestedContent property editor
17+
/// </summary>
18+
public class NestedContentValueConnector : IValueConnector
19+
{
20+
private readonly IContentTypeService _contentTypeService;
21+
private readonly Lazy<ValueConnectorCollection> _valueConnectorsLazy;
22+
private readonly ILogger _logger;
23+
24+
public NestedContentValueConnector(IContentTypeService contentTypeService, Lazy<ValueConnectorCollection> valueConnectors, ILogger logger)
25+
{
26+
if (contentTypeService == null) throw new ArgumentNullException(nameof(contentTypeService));
27+
if (valueConnectors == null) throw new ArgumentNullException(nameof(valueConnectors));
28+
if (logger == null) throw new ArgumentNullException(nameof(logger));
29+
_contentTypeService = contentTypeService;
30+
_valueConnectorsLazy = valueConnectors;
31+
_logger = logger;
32+
}
33+
34+
// Our.Umbraco.NestedContent is the original NestedContent package
35+
// Umbraco.NestedContent is Core NestedContent (introduced in v7.7)
36+
public string ToArtifact(object value, PropertyType propertyType, ICollection<ArtifactDependency> dependencies)
37+
{
38+
var svalue = value as string;
39+
if (string.IsNullOrWhiteSpace(svalue))
40+
return null;
41+
42+
if (svalue.DetectIsJson() == false)
43+
return null;
44+
45+
var nestedContent = new List<NestedContentValue>();
46+
if (svalue.Trim().StartsWith("{"))
47+
nestedContent.Add(JsonConvert.DeserializeObject<NestedContentValue>(svalue));
48+
else
49+
nestedContent.AddRange(JsonConvert.DeserializeObject<NestedContentValue[]>(svalue));
50+
51+
if (nestedContent.All(x => x == null))
52+
return null;
53+
54+
var allContentTypes = nestedContent.Select(x => x.ContentTypeAlias)
55+
.Distinct()
56+
.ToDictionary(a => a, a => _contentTypeService.Get(a));
57+
58+
//Ensure all of these content types are found
59+
if (allContentTypes.Values.Any(contentType => contentType == null))
60+
{
61+
throw new InvalidOperationException($"Could not resolve these content types for the Nested Content property: {string.Join(",", allContentTypes.Where(x => x.Value == null).Select(x => x.Key))}");
62+
}
63+
64+
//Ensure that these content types have dependencies added
65+
foreach (var contentType in allContentTypes.Values)
66+
{
67+
dependencies.Add(new ArtifactDependency(contentType.GetUdi(), false, ArtifactDependencyMode.Match));
68+
}
69+
70+
foreach (var row in nestedContent)
71+
{
72+
var contentType = allContentTypes[row.ContentTypeAlias];
73+
74+
foreach (var key in row.PropertyValues.Keys.ToArray())
75+
{
76+
// key is a system property that is added by NestedContent in Core v7.7
77+
// see note in NestedContentValue - leave it unchanged
78+
if (key == "key")
79+
continue;
80+
81+
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == key);
82+
83+
if (propType == null)
84+
{
85+
_logger.Debug<NestedContentValueConnector>($"No property type found with alias {key} on content type {contentType.Alias}");
86+
continue;
87+
}
88+
89+
// throws if not found - no need for a null check
90+
var propValueConnector = ValueConnectors.Get(propType);
91+
92+
// this should be enough for all other value connectors to work with
93+
// as all they should need is the value, and the property type infos
94+
//var mockProperty = new Property(propType);
95+
//var mockProperty = new Property(propType, row.PropertyValues[key]);
96+
var val = row.PropertyValues[key];
97+
object parsedValue = propValueConnector.ToArtifact(val, propType, dependencies);
98+
99+
// getting Map image value umb://media/43e7401fb3cd48ceaa421df511ec703c to (nothing) - why?!
100+
_logger.Debug<NestedContentValueConnector>("Map " + key + " value '" + row.PropertyValues[key] + "' to '" + parsedValue
101+
+ "' using " + propValueConnector.GetType() + " for " + propType);
102+
103+
// test if the value is a json object (thus could be a nested complex editor)
104+
// if that's the case we'll need to add it as a json object instead of string to avoid it being escaped
105+
JToken jtokenValue = parsedValue != null && parsedValue.ToString().DetectIsJson() ? JToken.Parse(parsedValue.ToString()) : null;
106+
if (jtokenValue != null)
107+
{
108+
parsedValue = jtokenValue;
109+
}
110+
else if (parsedValue != null)
111+
{
112+
parsedValue = parsedValue.ToString();
113+
}
114+
115+
row.PropertyValues[key] = parsedValue;
116+
}
117+
}
118+
119+
value = JsonConvert.SerializeObject(nestedContent);
120+
return (string)value;
121+
}
122+
123+
public object FromArtifact(string value, PropertyType propertyType, object currentValue)
124+
{
125+
if (string.IsNullOrWhiteSpace(value))
126+
{
127+
return value;
128+
}
129+
130+
if (value.DetectIsJson() == false)
131+
return value;
132+
133+
var nestedContent = JsonConvert.DeserializeObject<NestedContentValue[]>(value);
134+
135+
if (nestedContent == null)
136+
return value;
137+
138+
var allContentTypes = nestedContent.Select(x => x.ContentTypeAlias)
139+
.Distinct()
140+
.ToDictionary(a => a, a => _contentTypeService.Get(a));
141+
142+
//Ensure all of these content types are found
143+
if (allContentTypes.Values.Any(contentType => contentType == null))
144+
{
145+
throw new InvalidOperationException($"Could not resolve these content types for the Nested Content property: {string.Join(",", allContentTypes.Where(x => x.Value == null).Select(x => x.Key))}");
146+
}
147+
148+
var mocks = new Dictionary<IContentType, IContent>();
149+
150+
foreach (var row in nestedContent)
151+
{
152+
var contentType = allContentTypes[row.ContentTypeAlias];
153+
154+
// note
155+
// the way we do it here, doing content.SetValue() several time on the same content, reduces
156+
// allocations and should be ok because SetValue does not care about the previous value - would
157+
// be different for the overloads that manage eg files for uploads (not sure how NestedContent
158+
// deals with them really)
159+
160+
// we need a fake content instance to pass in to the value connector, since the value connector
161+
// wants to SetValue on an object - then we can extract the value back from that object to set
162+
// it correctly on the real instance
163+
IContent mockContent;
164+
if (!mocks.TryGetValue(contentType, out mockContent))
165+
mockContent = mocks[contentType] = new Content("NC_" + Guid.NewGuid(), -1, contentType);
166+
167+
foreach (var key in row.PropertyValues.Keys.ToArray())
168+
{
169+
// key is a system property that is added by NestedContent in Core v7.7
170+
// see note in NestedContentValue - leave it unchanged
171+
if (key == "key")
172+
continue;
173+
174+
var innerPropertyType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == key);
175+
176+
if (innerPropertyType == null)
177+
{
178+
_logger.Debug<NestedContentValueConnector>($"No property type found with alias {key} on content type {contentType.Alias}");
179+
continue;
180+
}
181+
182+
// throws if not found - no need for a null check
183+
var propValueConnector = ValueConnectors.Get(innerPropertyType);
184+
185+
var rowValue = row.PropertyValues[key];
186+
187+
if (rowValue != null)
188+
{
189+
var convertedValue = propValueConnector.FromArtifact(rowValue.ToString(), innerPropertyType, null);
190+
if (convertedValue == null)
191+
{
192+
row.PropertyValues[key] = null;
193+
}
194+
// integers needs to be converted into strings
195+
else if (convertedValue is int)
196+
{
197+
row.PropertyValues[key] = convertedValue.ToString();
198+
}
199+
else
200+
{
201+
// test if the value is a json object (thus could be a nested complex editor)
202+
// if that's the case we'll need to add it as a json object instead of string to avoid it being escaped
203+
JToken jtokenValue = convertedValue.ToString().DetectIsJson() ? JToken.Parse(convertedValue.ToString()) : null;
204+
if (jtokenValue != null)
205+
{
206+
row.PropertyValues[key] = jtokenValue;
207+
}
208+
else
209+
{
210+
row.PropertyValues[key] = convertedValue;
211+
}
212+
}
213+
}
214+
else
215+
{
216+
row.PropertyValues[key] = rowValue;
217+
}
218+
}
219+
}
220+
221+
// Note: NestedContent does not use formatting when serializing JSON values.
222+
value = JArray.FromObject(nestedContent).ToString(Formatting.None);
223+
return value;
224+
}
225+
226+
public virtual IEnumerable<string> PropertyEditorAliases => new[] { "Our.Umbraco.NestedContent", "Umbraco.NestedContent" };
227+
228+
// cannot inject ValueConnectorCollection else of course it creates a circular (recursive) dependency,
229+
// so we have to inject it lazily and use the lazy value when actually needing it
230+
private ValueConnectorCollection ValueConnectors => _valueConnectorsLazy.Value;
231+
232+
/// <summary>
233+
/// The typed value stored for Nested Content
234+
/// </summary>
235+
/// <example>
236+
/// An example of the JSON stored for NestedContent is:
237+
/// <![CDATA[
238+
/// [
239+
/// {"name":"Content","ncContentTypeAlias":"nC1","text":"Hello","multiText":"world","rTE":"<p>asdfasdfasdfasdf</p>\n<p>asdf</p>\n<p><img style=\"width: 213px; height: 213px;\" src=\"/media/1050/profile_pic_cg_2015.jpg?width=213&amp;height=213\" alt=\"\" rel=\"1087\" data-id=\"1087\" /></p>\n<p>asdf</p>"},
240+
/// {"name":"Content","ncContentTypeAlias":"nC1","text":"This is ","multiText":"pretty cool","rTE":""}
241+
/// ]
242+
/// ]]>
243+
/// </example>
244+
public class NestedContentValue
245+
{
246+
[JsonProperty("name")]
247+
public string Name { get; set; }
248+
249+
[JsonProperty("ncContentTypeAlias")]
250+
public string ContentTypeAlias { get; set; }
251+
252+
// starting with v7.7, Core's NestedContent implement "key" as a system property
253+
// but since we are supporting pre-v7.7 including the NestedContent package, we
254+
// cannot do it this way - it's all managed "manually" when dealing with
255+
// PropertyValues.
256+
//[JsonProperty("key")]
257+
//public Guid Key { get; set; }
258+
259+
/// <summary>
260+
/// The remaining properties will be serialized to a dictionary
261+
/// </summary>
262+
/// <remarks>
263+
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
264+
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
265+
/// NestedContent serializes to string, int, whatever eg
266+
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
267+
/// </remarks>
268+
[JsonExtensionData]
269+
public IDictionary<string, object> PropertyValues { get; set; }
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)