|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Xml;
namespace System.ServiceModel.Syndication
{
// NOTE: This class implements Clone so if you add any members, please update the copy ctor
public class SyndicationFeed : IExtensibleSyndicationObject
{
private static readonly HashSet<string> s_acceptedDays = new HashSet<string>(
new string[] { "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday" },
StringComparer.OrdinalIgnoreCase
);
private Collection<SyndicationPerson> _authors;
private Collection<SyndicationCategory> _categories;
private Collection<SyndicationPerson> _contributors;
private ExtensibleSyndicationObject _extensions;
private IEnumerable<SyndicationItem> _items;
private DateTimeOffset _lastUpdatedTime;
private Collection<SyndicationLink> _links;
public SyndicationFeed() : this((IEnumerable<SyndicationItem>)null)
{
}
public SyndicationFeed(IEnumerable<SyndicationItem> items) : this(null, null, null, items)
{
}
public SyndicationFeed(string title, string description, Uri feedAlternateLink)
: this(title, description, feedAlternateLink, null)
{
}
public SyndicationFeed(string title, string description, Uri feedAlternateLink, IEnumerable<SyndicationItem> items)
: this(title, description, feedAlternateLink, null, DateTimeOffset.MinValue, items)
{
}
public SyndicationFeed(string title, string description, Uri feedAlternateLink, string id, DateTimeOffset lastUpdatedTime)
: this(title, description, feedAlternateLink, id, lastUpdatedTime, null)
{
}
public SyndicationFeed(string title, string description, Uri feedAlternateLink, string id, DateTimeOffset lastUpdatedTime, IEnumerable<SyndicationItem> items)
{
if (title != null)
{
Title = new TextSyndicationContent(title);
}
if (description != null)
{
Description = new TextSyndicationContent(description);
}
if (feedAlternateLink != null)
{
Links.Add(SyndicationLink.CreateAlternateLink(feedAlternateLink));
}
Id = id;
_lastUpdatedTime = lastUpdatedTime;
_items = items;
}
protected SyndicationFeed(SyndicationFeed source, bool cloneItems)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
_authors = FeedUtils.ClonePersons(source._authors);
_categories = FeedUtils.CloneCategories(source._categories);
_contributors = FeedUtils.ClonePersons(source._contributors);
Copyright = FeedUtils.CloneTextContent(source.Copyright);
Description = FeedUtils.CloneTextContent(source.Description);
_extensions = source._extensions.Clone();
Generator = source.Generator;
Id = source.Id;
ImageUrl = source.ImageUrl;
Language = source.Language;
_lastUpdatedTime = source._lastUpdatedTime;
_links = FeedUtils.CloneLinks(source._links);
Title = FeedUtils.CloneTextContent(source.Title);
BaseUri = source.BaseUri;
if (source._items is IList<SyndicationItem> srcList)
{
Collection<SyndicationItem> tmp = new NullNotAllowedCollection<SyndicationItem>();
for (int i = 0; i < srcList.Count; ++i)
{
tmp.Add((cloneItems) ? srcList[i].Clone() : srcList[i]);
}
_items = tmp;
}
else
{
if (cloneItems)
{
throw new InvalidOperationException(SR.UnbufferedItemsCannotBeCloned);
}
_items = source._items;
}
}
public Dictionary<XmlQualifiedName, string> AttributeExtensions => _extensions.AttributeExtensions;
public Collection<SyndicationPerson> Authors
{
get => _authors ??= new NullNotAllowedCollection<SyndicationPerson>();
}
public Uri BaseUri { get; set; }
public Collection<SyndicationCategory> Categories
{
get => _categories ??= new NullNotAllowedCollection<SyndicationCategory>();
}
public Collection<SyndicationPerson> Contributors
{
get => _contributors ??= new NullNotAllowedCollection<SyndicationPerson>();
}
public TextSyndicationContent Copyright { get; set; }
public TextSyndicationContent Description { get; set; }
public SyndicationElementExtensionCollection ElementExtensions => _extensions.ElementExtensions;
public string Generator { get; set; }
public string Id { get; set; }
public Uri ImageUrl { get; set; }
public IEnumerable<SyndicationItem> Items
{
get => _items ??= new NullNotAllowedCollection<SyndicationItem>();
set => _items = value ?? throw new ArgumentNullException(nameof(value));
}
public string Language { get; set; }
internal Exception LastUpdatedTimeException { get; set; }
public DateTimeOffset LastUpdatedTime
{
get
{
if (LastUpdatedTimeException != null)
{
throw LastUpdatedTimeException;
}
return _lastUpdatedTime;
}
set
{
LastUpdatedTimeException = null;
_lastUpdatedTime = value;
}
}
public Collection<SyndicationLink> Links
{
get => _links ??= new NullNotAllowedCollection<SyndicationLink>();
}
public TextSyndicationContent Title { get; set; }
internal SyndicationLink InternalDocumentation { get; private set; }
public SyndicationLink Documentation
{
get => InternalDocumentation ??= TryReadDocumentationFromExtension(ElementExtensions);
set => InternalDocumentation = value;
}
internal TimeSpan? InternalTimeToLive { get; private set; }
public TimeSpan? TimeToLive
{
get => InternalTimeToLive ??= TryReadTimeToLiveFromExtension(ElementExtensions);
set
{
if (value.HasValue && (value.Value.Milliseconds != 0 || value.Value.Seconds != 0 || value.Value.TotalMinutes < 0))
{
throw new ArgumentOutOfRangeException(nameof(value), value.Value, SR.InvalidTimeToLiveValue);
}
InternalTimeToLive = value;
}
}
internal Collection<int> InternalSkipHours { get; private set; }
public Collection<int> SkipHours
{
get
{
if (InternalSkipHours == null)
{
var skipHours = new Collection<int>();
TryReadSkipHoursFromExtension(ElementExtensions, skipHours);
InternalSkipHours = skipHours;
}
return InternalSkipHours;
}
}
internal Collection<string> InternalSkipDays { get; private set; }
public Collection<string> SkipDays
{
get
{
if (InternalSkipDays == null)
{
var skipDays = new Collection<string>();
TryReadSkipDaysFromExtension(ElementExtensions, skipDays);
InternalSkipDays = skipDays;
}
return InternalSkipDays;
}
}
internal SyndicationTextInput InternalTextInput { get; private set; }
public SyndicationTextInput TextInput
{
get => InternalTextInput ??= TryReadTextInputFromExtension(ElementExtensions);
set => InternalTextInput = value;
}
private SyndicationLink TryReadDocumentationFromExtension(SyndicationElementExtensionCollection elementExtensions)
{
SyndicationElementExtension documentationElement = elementExtensions
.FirstOrDefault(e => e.OuterName == Rss20Constants.DocumentationTag && e.OuterNamespace == Rss20Constants.Rss20Namespace);
if (documentationElement == null)
return null;
using (XmlReader reader = documentationElement.GetReader())
{
SyndicationLink documentation = Rss20FeedFormatter.ReadAlternateLink(reader, BaseUri, SyndicationFeedFormatter.DefaultUriParser, preserveAttributeExtensions: true);
return documentation;
}
}
private static TimeSpan? TryReadTimeToLiveFromExtension(SyndicationElementExtensionCollection elementExtensions)
{
SyndicationElementExtension timeToLiveElement = elementExtensions
.FirstOrDefault(e => e.OuterName == Rss20Constants.TimeToLiveTag && e.OuterNamespace == Rss20Constants.Rss20Namespace);
if (timeToLiveElement == null)
return null;
using (XmlReader reader = timeToLiveElement.GetReader())
{
string value = reader.ReadElementString();
if (int.TryParse(value, out int timeToLive))
{
if (timeToLive >= 0)
{
return TimeSpan.FromMinutes(timeToLive);
}
else
{
return null;
}
}
else
{
return null;
}
}
}
private static void TryReadSkipHoursFromExtension(SyndicationElementExtensionCollection elementExtensions, Collection<int> skipHours)
{
SyndicationElementExtension skipHoursElement = elementExtensions
.FirstOrDefault(e => e.OuterName == Rss20Constants.SkipHoursTag && e.OuterNamespace == Rss20Constants.Rss20Namespace);
if (skipHoursElement == null)
return;
using (XmlReader reader = skipHoursElement.GetReader())
{
reader.ReadStartElement();
while (reader.IsStartElement())
{
if (reader.LocalName == Rss20Constants.HourTag)
{
string value = reader.ReadElementString();
bool parsed = int.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int hour);
if (!parsed || (hour < 0 || hour > 23))
{
throw new FormatException(SR.Format(SR.InvalidSkipHourValue, value));
}
skipHours.Add(hour);
}
else
{
reader.Skip();
}
}
}
}
private static void TryReadSkipDaysFromExtension(SyndicationElementExtensionCollection elementExtensions, Collection<string> skipDays)
{
SyndicationElementExtension skipDaysElement = elementExtensions
.FirstOrDefault(e => e.OuterName == Rss20Constants.SkipDaysTag && e.OuterNamespace == Rss20Constants.Rss20Namespace);
if (skipDaysElement == null)
return;
using (XmlReader reader = skipDaysElement.GetReader())
{
reader.ReadStartElement();
while (reader.IsStartElement())
{
if (reader.LocalName == Rss20Constants.DayTag)
{
string day = reader.ReadElementString();
// Check if the day is actually an accepted day.
if (IsValidDay(day))
{
skipDays.Add(day);
}
}
else
{
reader.Skip();
}
}
reader.ReadEndElement();
}
}
private static bool IsValidDay(string day) => s_acceptedDays.Contains(day);
private static SyndicationTextInput TryReadTextInputFromExtension(SyndicationElementExtensionCollection elementExtensions)
{
SyndicationElementExtension textInputElement = elementExtensions
.FirstOrDefault(e => e.OuterName == Rss20Constants.TextInputTag && e.OuterNamespace == Rss20Constants.Rss20Namespace);
if (textInputElement == null)
return null;
var textInput = new SyndicationTextInput();
using (XmlReader reader = textInputElement.GetReader())
{
reader.ReadStartElement();
while (reader.IsStartElement())
{
string name = reader.LocalName;
string value = reader.ReadElementString();
switch (name)
{
case Rss20Constants.DescriptionTag:
textInput.Description = value;
break;
case Rss20Constants.TitleTag:
textInput.Title = value;
break;
case Rss20Constants.LinkTag:
textInput.Link = new SyndicationLink(new Uri(value, UriKind.RelativeOrAbsolute));
break;
case Rss20Constants.NameTag:
textInput.Name = value;
break;
default:
break;
}
}
reader.ReadEndElement();
}
return IsValidTextInput(textInput) ? textInput : null;
}
private static bool IsValidTextInput(SyndicationTextInput textInput)
{
// All textInput items are required, we check if all items were instantiated.
return textInput.Description != null && textInput.Title != null && textInput.Name != null && textInput.Link != null;
}
public static SyndicationFeed Load(XmlReader reader) => Load<SyndicationFeed>(reader);
public static TSyndicationFeed Load<TSyndicationFeed>(XmlReader reader) where TSyndicationFeed : SyndicationFeed, new()
{
if (reader is null)
{
throw new ArgumentNullException(nameof(reader));
}
Atom10FeedFormatter<TSyndicationFeed> atomSerializer = new Atom10FeedFormatter<TSyndicationFeed>();
if (atomSerializer.CanRead(reader))
{
atomSerializer.ReadFrom(reader);
return atomSerializer.Feed as TSyndicationFeed;
}
Rss20FeedFormatter<TSyndicationFeed> rssSerializer = new Rss20FeedFormatter<TSyndicationFeed>();
if (rssSerializer.CanRead(reader))
{
rssSerializer.ReadFrom(reader);
return rssSerializer.Feed as TSyndicationFeed;
}
throw new XmlException(SR.Format(SR.UnknownFeedXml, reader.LocalName, reader.NamespaceURI));
}
public virtual SyndicationFeed Clone(bool cloneItems)
{
return new SyndicationFeed(this, cloneItems);
}
public Atom10FeedFormatter GetAtom10Formatter() => new Atom10FeedFormatter(this);
public Rss20FeedFormatter GetRss20Formatter() => GetRss20Formatter(true);
public Rss20FeedFormatter GetRss20Formatter(bool serializeExtensionsAsAtom)
{
return new Rss20FeedFormatter(this, serializeExtensionsAsAtom);
}
public void SaveAsAtom10(XmlWriter writer)
{
GetAtom10Formatter().WriteTo(writer);
}
public void SaveAsRss20(XmlWriter writer)
{
GetRss20Formatter().WriteTo(writer);
}
protected internal virtual SyndicationCategory CreateCategory()
{
return new SyndicationCategory();
}
protected internal virtual SyndicationItem CreateItem()
{
return new SyndicationItem();
}
protected internal virtual SyndicationLink CreateLink()
{
return new SyndicationLink();
}
protected internal virtual SyndicationPerson CreatePerson()
{
return new SyndicationPerson();
}
protected internal virtual bool TryParseAttribute(string name, string ns, string value, string version)
{
return false;
}
protected internal virtual bool TryParseElement(XmlReader reader, string version)
{
return false;
}
protected internal virtual void WriteAttributeExtensions(XmlWriter writer, string version)
{
_extensions.WriteAttributeExtensions(writer);
}
protected internal virtual void WriteElementExtensions(XmlWriter writer, string version)
{
_extensions.WriteElementExtensions(writer, ShouldSkipWritingElements);
}
private bool ShouldSkipWritingElements(string localName, string ns)
{
if (ns == Rss20Constants.Rss20Namespace)
{
switch (localName)
{
case Rss20Constants.DocumentationTag:
return InternalDocumentation != null;
case Rss20Constants.TimeToLiveTag:
return InternalTimeToLive != null;
case Rss20Constants.TextInputTag:
return InternalTextInput != null;
case Rss20Constants.SkipHoursTag:
return InternalSkipHours != null;
case Rss20Constants.SkipDaysTag:
return InternalSkipDays != null;
}
}
return false;
}
internal void LoadElementExtensions(XmlReader readerOverUnparsedExtensions, int maxExtensionSize)
{
_extensions.LoadElementExtensions(readerOverUnparsedExtensions, maxExtensionSize);
}
internal void LoadElementExtensions(XmlBuffer buffer)
{
_extensions.LoadElementExtensions(buffer);
}
}
}
|