Skip to content

Commit 0ed92ec

Browse files
committed
Fix GoogleMerchantCenterFeedService to make use of multiple value property extractors (#29)
* Add comments, rename variables and params * change some wordings * Rework GoogleMerchantCenterFeedService to actually make use of IMultipleValuePropertyExtractor * refactor how we're adding a new xml node * obsolete extension method and give a heads up about its namespace changing.
1 parent 54a4d7e commit 0ed92ec

File tree

11 files changed

+160
-61
lines changed

11 files changed

+160
-61
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Xml;
2+
3+
namespace Umbraco.Commerce.ProductFeeds.Core.Extensions
4+
{
5+
internal static class XmlElementExtensions
6+
{
7+
/// <summary>
8+
/// Add a new child node at the bottom of the list of child nodes, of this node, only when the child node inner text has value.
9+
/// </summary>
10+
/// <param name="element"></param>
11+
/// <param name="childNodeName"></param>
12+
/// <param name="childNodeInnerText"></param>
13+
/// <param name="xmlNameSpace"></param>
14+
/// <returns>The added child node or null.</returns>
15+
public static XmlElement? AddChild(this XmlElement element, string childNodeName, string childNodeInnerText, string? xmlNameSpace = null)
16+
{
17+
if (!string.IsNullOrWhiteSpace(childNodeInnerText))
18+
{
19+
XmlElement childNode = element.OwnerDocument.CreateElement(childNodeName, xmlNameSpace);
20+
childNode.InnerText = childNodeInnerText;
21+
element.AppendChild(childNode);
22+
return childNode;
23+
}
24+
25+
return null;
26+
}
27+
}
28+
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/FeedGenerators/Implementations/GoogleMerchantCenterFeedService.cs

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
using Umbraco.Commerce.Core.Models;
99
using Umbraco.Commerce.Core.Services;
1010
using Umbraco.Commerce.Extensions;
11+
using Umbraco.Commerce.ProductFeeds.Core.Extensions;
1112
using Umbraco.Commerce.ProductFeeds.Core.Features.FeedGenerators.Implementations;
1213
using Umbraco.Commerce.ProductFeeds.Core.Features.FeedSettings.Application;
13-
using Umbraco.Commerce.ProductFeeds.Core.Features.PropertyValueExtractors.Implementations;
1414
using Umbraco.Commerce.ProductFeeds.Core.FeedGenerators.Application;
1515
using Umbraco.Commerce.ProductFeeds.Core.ProductQueries.Application;
1616
using Umbraco.Commerce.ProductFeeds.Core.PropertyValueExtractors.Application;
1717
using Umbraco.Commerce.ProductFeeds.Extensions;
1818

1919
namespace Umbraco.Commerce.ProductFeeds.Core.FeedGenerators.Implementations
2020
{
21+
/// <summary>
22+
/// This is the feed generator that follows Google Merchant Center's standard.
23+
/// </summary>
2124
public class GoogleMerchantCenterFeedService : IProductFeedGeneratorService
2225
{
2326
private const string GoogleXmlNamespaceUri = "http://base.google.com/ns/1.0";
@@ -50,6 +53,12 @@ public GoogleMerchantCenterFeedService(
5053
_multipleValuePropertyExtractorFactory = multipleValuePropertyExtractorFactory;
5154
}
5255

56+
/// <summary>
57+
/// Generate the product feed following the inputted settings.
58+
/// </summary>
59+
/// <param name="feedSetting"></param>
60+
/// <returns></returns>
61+
/// <exception cref="IdPropertyNodeMappingNotFoundException"></exception>
5362
public XmlDocument GenerateFeed(ProductFeedSettingReadModel feedSetting)
5463
{
5564
ArgumentNullException.ThrowIfNull(feedSetting, nameof(feedSetting));
@@ -65,14 +74,10 @@ public XmlDocument GenerateFeed(ProductFeedSettingReadModel feedSetting)
6574
XmlElement channel = doc.CreateElement("channel");
6675

6776
// doc/channel/title
68-
XmlElement titleNode = channel.OwnerDocument.CreateElement("title");
69-
titleNode.InnerText = feedSetting.FeedName;
70-
channel.AppendChild(titleNode);
77+
channel.AddChild("title", feedSetting.FeedName);
7178

7279
// doc/channel/description
73-
XmlElement descriptionNode = channel.OwnerDocument.CreateElement("description");
74-
descriptionNode.InnerText = feedSetting.FeedDescription;
75-
channel.AppendChild(descriptionNode);
80+
channel.AddChild("description", feedSetting.FeedDescription);
7681
root.AppendChild(channel);
7782

7883
ICollection<IPublishedContent> products = _productQueryService.GetPublishedProducts(new GetPublishedProductsParams
@@ -142,9 +147,9 @@ public XmlDocument GenerateFeed(ProductFeedSettingReadModel feedSetting)
142147
/// Create a new node for the one, in this case, each product/variant is one &lt;item&gt; node.
143148
/// </summary>
144149
/// <param name="feedSetting"></param>
145-
/// <param name="channel"></param>
146-
/// <param name="variant"></param>
147-
/// <param name="mainProduct"></param>
150+
/// <param name="channel">The XML &lt;channel&gt; node that holds the &lt;item&gt; node.</param>
151+
/// <param name="variant">Product variant.</param>
152+
/// <param name="mainProduct">Main product which may have multiple variants. Some common properties can only be found in the main product, not in a variant.</param>
148153
/// <returns></returns>
149154
private XmlElement NewItemNode(ProductFeedSettingReadModel feedSetting, XmlElement channel, IPublishedElement variant, IPublishedElement? mainProduct)
150155
{
@@ -153,21 +158,32 @@ private XmlElement NewItemNode(ProductFeedSettingReadModel feedSetting, XmlEleme
153158
// add custom properties
154159
foreach (PropertyAndNodeMapItem map in feedSetting.PropertyNameMappings)
155160
{
156-
if (map.ValueExtractorName == nameof(DefaultMultipleMediaPickerPropertyValueExtractor))
161+
if (_singleValuePropertyExtractorFactory.TryGetExtractor(map.ValueExtractorId, out ISingleValuePropertyExtractor? singleValueExtractor)
162+
&& singleValueExtractor != null)
157163
{
158-
AddImageNodes(itemNode, map.ValueExtractorName, map.PropertyAlias, variant, mainProduct);
164+
string propValue = singleValueExtractor.Extract(variant, map.PropertyAlias, mainProduct);
165+
itemNode.AddChild(map.NodeName, propValue, GoogleXmlNamespaceUri);
159166
}
160-
else
167+
else if (_multipleValuePropertyExtractorFactory.TryGetExtractor(map.ValueExtractorId!, out IMultipleValuePropertyExtractor? multipleValueExtractor)
168+
&& multipleValueExtractor != null)
161169
{
162-
ISingleValuePropertyExtractor valueExtractor = _singleValuePropertyExtractorFactory.GetExtractor(map.ValueExtractorName);
163-
string propValue = valueExtractor.Extract(variant, map.PropertyAlias, mainProduct);
164-
if (!string.IsNullOrWhiteSpace(propValue))
170+
var values = multipleValueExtractor.Extract(variant, map.PropertyAlias, mainProduct).ToList();
171+
if (map.NodeName.Equals("g:image_link", StringComparison.OrdinalIgnoreCase))
172+
{
173+
AddImageNodes(itemNode, values);
174+
}
175+
else
165176
{
166-
XmlElement propertyNode = itemNode.OwnerDocument.CreateElement(map.NodeName, GoogleXmlNamespaceUri);
167-
propertyNode.InnerText = propValue;
168-
itemNode.AppendChild(propertyNode);
177+
foreach (string value in values)
178+
{
179+
itemNode.AddChild(map.NodeName, value, GoogleXmlNamespaceUri);
180+
}
169181
}
170182
}
183+
else
184+
{
185+
throw new InvalidOperationException($"Unable to locate property value extractor with id = '{map.ValueExtractorId}'");
186+
}
171187
}
172188

173189
if (mainProduct == null) // when the variant is the main product itself
@@ -181,27 +197,23 @@ private XmlElement NewItemNode(ProductFeedSettingReadModel feedSetting, XmlEleme
181197

182198
private static void AddItemGroupNode(XmlElement itemNode, string groupId)
183199
{
184-
XmlElement availabilityNode = itemNode.OwnerDocument.CreateElement("g:item_group_id", GoogleXmlNamespaceUri);
185-
availabilityNode.InnerText = groupId;
186-
itemNode.AppendChild(availabilityNode);
200+
itemNode.AddChild("g:item_group_id", groupId, GoogleXmlNamespaceUri);
187201
}
188202

189203
/// <summary>
190204
/// Add a &lt;url&gt; node under the provided &lt;item&gt; node.
191205
/// </summary>
192-
/// <param name="itemNode"></param>
206+
/// <param name="itemNode">The XML item node that represents the current product.</param>
193207
/// <param name="product"></param>
194208
private static void AddUrlNode(XmlElement itemNode, IPublishedContent product)
195209
{
196-
XmlElement linkNode = itemNode.OwnerDocument.CreateElement("g:link", GoogleXmlNamespaceUri);
197-
linkNode.InnerText = product.Url(mode: UrlMode.Absolute);
198-
itemNode.AppendChild(linkNode);
210+
itemNode.AddChild("g:link", product.Url(mode: UrlMode.Absolute), GoogleXmlNamespaceUri);
199211
}
200212

201213
/// <summary>
202214
/// Add a &lt;price&gt; node under the provided &lt;item&gt; node.
203215
/// </summary>
204-
/// <param name="itemNode"></param>
216+
/// <param name="itemNode">The XML item node that represents the current product.</param>
205217
/// <param name="feedSetting"></param>
206218
/// <param name="product"></param>
207219
/// <param name="complexVariant"></param>
@@ -223,38 +235,28 @@ private void AddPriceNode(XmlElement itemNode, ProductFeedSettingReadModel feedS
223235
Attempt<Price> calculatePriceAttempt = productSnapshot.TryCalculatePrice();
224236
Price calculatedPrice = calculatePriceAttempt.Success ? calculatePriceAttempt.Result! : throw new NotImplementedException("Failed to calculate the price");
225237
decimal priceForShow = feedSetting.IncludeTaxInPrice ? calculatedPrice.WithTax : calculatedPrice.WithoutTax;
226-
priceNode.InnerText = $"{priceForShow.ToString("0.00", CultureInfo.InvariantCulture)} {_currencyService.GetCurrency(calculatedPrice.CurrencyId).Code}";
227-
itemNode.AppendChild(priceNode);
238+
string formattedPrice = $"{priceForShow.ToString("0.00", CultureInfo.InvariantCulture)} {_currencyService.GetCurrency(calculatedPrice.CurrencyId).Code}";
239+
itemNode.AddChild("g:price", formattedPrice, GoogleXmlNamespaceUri);
228240
}
229241

230242
/// <summary>
231-
/// Add image nodes under the provided &lt;item&gt; node.
243+
/// This method adds appropriate image nodes under the provided &lt;item&gt; node. It exists because Google Merchant Center treats multiple product images abnormally.
232244
/// </summary>
233-
/// <param name="itemNode"></param>
234-
/// <param name="valueExtractorName"></param>
235-
/// <param name="propertyAlias"></param>
236-
/// <param name="product"></param>
237-
/// <param name="mainProduct"></param>
238-
private void AddImageNodes(XmlElement itemNode, string valueExtractorName, string propertyAlias, IPublishedElement product, IPublishedElement? mainProduct)
245+
/// <param name="itemNode">The XML item node that represents the current product.</param>
246+
/// <param name="imageUrls"></param>
247+
private static void AddImageNodes(XmlElement itemNode, List<string> imageUrls)
239248
{
240-
IMultipleValuePropertyExtractor multipleValuePropertyExtractor = _multipleValuePropertyExtractorFactory.GetExtractor(valueExtractorName);
241-
var imageUrls = multipleValuePropertyExtractor.Extract(product, propertyAlias, mainProduct).ToList();
242-
if (imageUrls.Count <= 0)
249+
if (imageUrls.Count == 0)
243250
{
244251
return;
245252
}
246253

247-
XmlElement imageNode = itemNode.OwnerDocument.CreateElement("g:image_link", GoogleXmlNamespaceUri);
248-
imageNode.InnerText = imageUrls.First();
249-
itemNode.AppendChild(imageNode);
250-
254+
itemNode.AddChild("g:image_link", imageUrls.First(), GoogleXmlNamespaceUri);
251255
if (imageUrls.Count > 1)
252256
{
253257
for (int i = 1; i < imageUrls.Count; i++)
254258
{
255-
XmlElement additionalImageNode = itemNode.OwnerDocument.CreateElement("g:additional_image_link", GoogleXmlNamespaceUri);
256-
additionalImageNode.InnerText = imageUrls[i];
257-
itemNode.AppendChild(additionalImageNode);
259+
itemNode.AddChild("g:additional_image_link", imageUrls[i], GoogleXmlNamespaceUri);
258260
}
259261
}
260262
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/FeedSettings/Application/PropertyAndNodeMapItem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ public class PropertyAndNodeMapItem
1515
/// <summary>
1616
/// Property value extractor id.
1717
/// </summary>
18+
[Obsolete("Will be removed in v15. Use ValueExtractorId instead.")]
1819
public string? ValueExtractorName { get; set; }
20+
21+
/// <summary>
22+
/// Property value extractor id.
23+
/// </summary>
24+
public string? ValueExtractorId => ValueExtractorName;
1925
}
2026
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/PropertyValueExtractors/Application/IMultipleValuePropertyExtractor.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ namespace Umbraco.Commerce.ProductFeeds.Core.PropertyValueExtractors.Application
77
/// </summary>
88
public interface IMultipleValuePropertyExtractor
99
{
10+
/// <summary>
11+
/// The unique id of this extractor.
12+
/// </summary>
1013
string Id { get; }
1114

15+
/// <summary>
16+
/// The user friendly name of this extractor.
17+
/// </summary>
1218
string DisplayName { get; }
1319

1420
/// <summary>
15-
/// Get a list of absolute url of the media.
21+
/// Extracts the value from the property and returns them as a collection.
1622
/// </summary>
17-
/// <param name="content"></param>
23+
/// <param name="content">The umbraco content that holds the property.</param>
1824
/// <param name="propertyAlias"></param>
19-
/// <param name="fallbackElement"></param>
25+
/// <param name="fallbackElement">If the property alias can't be found on the main content, this method should try looking for it on the fallback content.</param>
2026
/// <returns></returns>
2127
ICollection<string> Extract(IPublishedElement content, string propertyAlias, IPublishedElement? fallbackElement);
2228
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/PropertyValueExtractors/Application/IMultipleValuePropertyExtractorFactory.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,17 @@ public interface IMultipleValuePropertyExtractorFactory
55
/// <summary>
66
/// Get the property extractor.
77
/// </summary>
8-
/// <param name="uniqueExtractorName"></param>
8+
/// <param name="valueExtractorId"></param>
99
/// <returns></returns>
10-
IMultipleValuePropertyExtractor GetExtractor(string uniqueExtractorName);
10+
[Obsolete("Will be removed in v15. Use TryGetExtractor instead.")]
11+
IMultipleValuePropertyExtractor GetExtractor(string valueExtractorId);
12+
13+
/// <summary>
14+
/// Try to get the property extractor. Returns the default extractor implementation if no extractor id is provided.
15+
/// </summary>
16+
/// <param name="valueExtractorId"></param>
17+
/// <param name="valueExtractor">The located value extractor. It can be null if no extractor was found.</param>
18+
/// <returns>True if an extractor was found, otherwise False.</returns>
19+
bool TryGetExtractor(string valueExtractorId, out IMultipleValuePropertyExtractor? valueExtractor) => throw new NotImplementedException();
1120
}
1221
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/PropertyValueExtractors/Application/ISingleValuePropertyExtractor.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ namespace Umbraco.Commerce.ProductFeeds.Core.PropertyValueExtractors.Application
77
/// </summary>
88
public interface ISingleValuePropertyExtractor
99
{
10+
/// <summary>
11+
/// Returns the value extractor id. Must be unique.
12+
/// </summary>
1013
public string Id { get; }
1114

15+
/// <summary>
16+
/// Returns a user friendly name of the value extractor.
17+
/// </summary>
1218
public string DisplayName { get; }
1319

1420
/// <summary>
15-
/// Get property value using property alias.
21+
/// Gets property value using property alias.
1622
/// </summary>
1723
/// <param name="content"></param>
1824
/// <param name="propertyAlias"></param>
19-
/// <param name="fallbackElement">Store fallback value of the property.</param>
25+
/// <param name="fallbackElement">Stores fallback value of the property.</param>
2026
/// <returns></returns>
2127
string Extract(IPublishedElement content, string propertyAlias, IPublishedElement? fallbackElement);
2228
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/PropertyValueExtractors/Application/ISingleValuePropertyExtractorFactory.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ namespace Umbraco.Commerce.ProductFeeds.Core.PropertyValueExtractors.Application
33
public interface ISingleValuePropertyExtractorFactory
44
{
55
/// <summary>
6-
/// Get the property extractor. Returns the default extractor implementation if no extractor name is provided.
6+
/// Get the property extractor. Returns the default extractor implementation if no extractor id is provided.
77
/// </summary>
88
/// <param name="extractorId"></param>
99
/// <returns></returns>
10+
[Obsolete("Will be removed in v15. Use TryGetExtractor instead.")]
1011
ISingleValuePropertyExtractor GetExtractor(string? extractorId = null);
12+
13+
/// <summary>
14+
/// Try to get the property extractor. Returns the default extractor implementation if no extractor id is provided.
15+
/// </summary>
16+
/// <param name="extractorId"></param>
17+
/// <param name="extractor">The located value extractor. It can be null if no extractor was found.</param>
18+
/// <returns>True if an extractor was found, otherwise False.</returns>
19+
bool TryGetExtractor(string? extractorId, out ISingleValuePropertyExtractor? extractor) => throw new NotImplementedException();
1120
}
1221
}

src/Umbraco.Commerce.ProductFeeds.Core/Features/PropertyValueExtractors/Implementations/MultipleValuePropertyExtractorFactory.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,30 @@ public MultipleValuePropertyExtractorFactory(MultipleValuePropertyExtractorColle
1212
}
1313

1414
/// <inheritdoc/>
15-
public IMultipleValuePropertyExtractor GetExtractor(string uniqueExtractorName)
15+
[Obsolete("Will be removed in v15. Use TryGetExtractor instead.")]
16+
public IMultipleValuePropertyExtractor GetExtractor(string valueExtractorId)
1617
{
17-
if (string.IsNullOrWhiteSpace(uniqueExtractorName))
18+
if (string.IsNullOrWhiteSpace(valueExtractorId))
1819
{
19-
throw new ArgumentNullException(nameof(uniqueExtractorName));
20+
throw new ArgumentNullException(nameof(valueExtractorId));
2021
}
2122

22-
23-
IMultipleValuePropertyExtractor? valueExtractor = _valueExtractors.FirstOrDefault(x => x.Id == uniqueExtractorName)
24-
?? throw new InvalidOperationException($"Can't find property extractor with name '{uniqueExtractorName}'");
23+
IMultipleValuePropertyExtractor? valueExtractor = _valueExtractors.FirstOrDefault(x => x.Id == valueExtractorId)
24+
?? throw new InvalidOperationException($"Can't find property extractor with id '{valueExtractorId}'");
2525

2626
return valueExtractor;
2727
}
28+
29+
/// <inheritdoc/>
30+
public bool TryGetExtractor(string valueExtractorId, out IMultipleValuePropertyExtractor? valueExtractor)
31+
{
32+
if (string.IsNullOrWhiteSpace(valueExtractorId))
33+
{
34+
throw new ArgumentNullException(nameof(valueExtractorId));
35+
}
36+
37+
valueExtractor = _valueExtractors.FirstOrDefault(x => x.Id == valueExtractorId);
38+
return valueExtractor != null ? true : false;
39+
}
2840
}
2941
}

0 commit comments

Comments
 (0)