2 // Rss20ItemFormatter.cs
5 // Atsushi Enomoto <atsushi@ximian.com>
7 // Copyright (C) 2007 Novell, Inc (http://www.novell.com)
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 using System
.Collections
.Generic
;
30 using System
.Collections
.ObjectModel
;
31 using System
.Globalization
;
34 using System
.Runtime
.Serialization
;
37 using System
.Xml
.Schema
;
38 using System
.Xml
.Serialization
;
40 namespace System
.ServiceModel
.Syndication
42 static class XmlReaderExtensions
44 public static bool IsTextNode (this XmlReader r
)
47 case XmlNodeType
.Text
:
48 case XmlNodeType
.CDATA
:
49 case XmlNodeType
.Whitespace
:
50 case XmlNodeType
.SignificantWhitespace
:
57 [XmlRoot ("item", Namespace
= "")]
58 public class Rss20ItemFormatter
: SyndicationItemFormatter
, IXmlSerializable
60 const string AtomNamespace
="http://www.w3.org/2005/Atom";
62 bool ext_atom_serialization
, preserve_att_ext
= true, preserve_elem_ext
= true;
65 public Rss20ItemFormatter ()
67 ext_atom_serialization
= true;
70 public Rss20ItemFormatter (SyndicationItem itemToWrite
)
71 : this (itemToWrite
, true)
75 public Rss20ItemFormatter (SyndicationItem itemToWrite
, bool serializeExtensionsAsAtom
)
78 ext_atom_serialization
= serializeExtensionsAsAtom
;
81 public Rss20ItemFormatter (Type itemTypeToCreate
)
83 if (itemTypeToCreate
== null)
84 throw new ArgumentNullException ("itemTypeToCreate");
85 item_type
= itemTypeToCreate
;
88 public bool SerializeExtensionsAsAtom
{
89 get { return ext_atom_serialization; }
90 set { ext_atom_serialization = value; }
93 protected Type ItemType
{
94 get { return item_type; }
97 public bool PreserveAttributeExtensions
{
98 get { return preserve_att_ext; }
99 set { preserve_att_ext = value; }
102 public bool PreserveElementExtensions
{
103 get { return preserve_elem_ext; }
104 set { preserve_elem_ext = value; }
107 public override string Version
{
108 get { return "Rss20"; }
111 protected override SyndicationItem
CreateItemInstance ()
113 return new SyndicationItem ();
116 public override bool CanRead (XmlReader reader
)
119 throw new ArgumentNullException ("reader");
120 reader
.MoveToContent ();
121 return reader
.IsStartElement ("item", String
.Empty
);
124 public override void ReadFrom (XmlReader reader
)
126 if (!CanRead (reader
))
127 throw new XmlException (String
.Format ("Element '{0}' in namespace '{1}' is not accepted by this syndication formatter", reader
.LocalName
, reader
.NamespaceURI
));
128 ReadXml (reader
, true);
131 public override void WriteTo (XmlWriter writer
)
133 WriteXml (writer
, true);
136 void IXmlSerializable
.ReadXml (XmlReader reader
)
138 ReadXml (reader
, false);
141 void IXmlSerializable
.WriteXml (XmlWriter writer
)
143 WriteXml (writer
, false);
146 XmlSchema IXmlSerializable
.GetSchema ()
153 void ReadXml (XmlReader reader
, bool fromSerializable
)
156 throw new ArgumentNullException ("reader");
158 SetItem (CreateItemInstance ());
160 reader
.MoveToContent ();
162 if (PreserveAttributeExtensions
&& reader
.MoveToFirstAttribute ()) {
164 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
166 if (!TryParseAttribute (reader
.LocalName
, reader
.NamespaceURI
, reader
.Value
, Item
, Version
))
167 Item
.AttributeExtensions
.Add (new XmlQualifiedName (reader
.LocalName
, reader
.NamespaceURI
), reader
.Value
);
168 } while (reader
.MoveToNextAttribute ());
171 reader
.ReadStartElement ();
173 for (reader
.MoveToContent (); reader
.NodeType
!= XmlNodeType
.EndElement
; reader
.MoveToContent ()) {
174 if (reader
.NodeType
!= XmlNodeType
.Element
)
175 throw new XmlException ("Only element node is expected under 'item' element");
176 if (reader
.NamespaceURI
== String
.Empty
)
177 switch (reader
.LocalName
) {
179 Item
.Title
= ReadTextSyndicationContent (reader
);
182 SyndicationLink l
= Item
.CreateLink ();
183 ReadLink (reader
, l
);
187 Item
.Summary
= ReadTextSyndicationContent (reader
);
190 SyndicationPerson p
= Item
.CreatePerson ();
191 ReadPerson (reader
, p
);
192 Item
.Authors
.Add (p
);
195 SyndicationCategory c
= Item
.CreateCategory ();
196 ReadCategory (reader
, c
);
197 Item
.Categories
.Add (c
);
199 // case "comments": // treated as extension ...
201 l
= Item
.CreateLink ();
202 ReadEnclosure (reader
, l
);
206 if (reader
.GetAttribute ("isPermaLink") == "true")
207 Item
.AddPermalink (CreateUri (reader
.ReadElementContentAsString ()));
209 Item
.Id
= reader
.ReadElementContentAsString ();
212 Item
.PublishDate
= FromRFC822DateString (reader
.ReadElementContentAsString ());
215 Item
.SourceFeed
= new SyndicationFeed ();
216 ReadSourceFeed (reader
, Item
.SourceFeed
);
219 else if (SerializeExtensionsAsAtom
&& reader
.NamespaceURI
== AtomNamespace
) {
220 switch (reader
.LocalName
) {
222 SyndicationPerson p
= Item
.CreatePerson ();
223 ReadPersonAtom10 (reader
, p
);
224 Item
.Contributors
.Add (p
);
227 Item
.LastUpdatedTime
= XmlConvert
.ToDateTimeOffset (reader
.ReadElementContentAsString ());
230 Item
.Copyright
= ReadTextSyndicationContent (reader
);
233 if (reader
.GetAttribute ("src") != null) {
234 Item
.Content
= new UrlSyndicationContent (CreateUri (reader
.GetAttribute ("src")), reader
.GetAttribute ("type"));
238 switch (reader
.GetAttribute ("type")) {
242 Item
.Content
= ReadTextSyndicationContent (reader
);
245 SyndicationContent content
;
246 if (!TryParseContent (reader
, Item
, reader
.GetAttribute ("type"), Version
, out content
))
247 Item
.Content
= new XmlSyndicationContent (reader
);
252 if (!TryParseElement (reader
, Item
, Version
)) {
253 if (PreserveElementExtensions
)
254 // FIXME: what to specify for maxExtensionSize
255 LoadElementExtensions (reader
, Item
, int.MaxValue
);
261 reader
.ReadEndElement (); // </item>
264 TextSyndicationContent
ReadTextSyndicationContent (XmlReader reader
)
266 TextSyndicationContentKind kind
= TextSyndicationContentKind
.Plaintext
;
267 switch (reader
.GetAttribute ("type")) {
269 kind
= TextSyndicationContentKind
.Html
;
272 kind
= TextSyndicationContentKind
.XHtml
;
275 string text
= reader
.ReadElementContentAsString ();
276 TextSyndicationContent t
= new TextSyndicationContent (text
, kind
);
280 void ReadCategory (XmlReader reader
, SyndicationCategory category
)
282 if (reader
.MoveToFirstAttribute ()) {
284 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
286 if (reader
.NamespaceURI
== String
.Empty
) {
287 switch (reader
.LocalName
) {
289 category
.Scheme
= reader
.Value
;
293 if (PreserveAttributeExtensions
)
294 if (!TryParseAttribute (reader
.LocalName
, reader
.NamespaceURI
, reader
.Value
, category
, Version
))
295 category
.AttributeExtensions
.Add (new XmlQualifiedName (reader
.LocalName
, reader
.NamespaceURI
), reader
.Value
);
296 } while (reader
.MoveToNextAttribute ());
297 reader
.MoveToElement ();
300 if (!reader
.IsEmptyElement
) {
302 for (reader
.MoveToContent (); reader
.NodeType
!= XmlNodeType
.EndElement
; reader
.MoveToContent ()) {
303 if (reader
.IsTextNode ())
304 category
.Name
+= reader
.Value
;
305 else if (!TryParseElement (reader
, category
, Version
)) {
306 if (PreserveElementExtensions
)
307 // FIXME: what should be used for maxExtenswionSize
308 LoadElementExtensions (reader
, category
, int.MaxValue
);
315 reader
.Read (); // </category> or <category ... />
318 // SyndicationLink.CreateMediaEnclosureLink() is almost
319 // useless here since it cannot handle extension attributes
320 // in straightforward way (it I use it, I have to iterate
321 // attributes twice just to read extensions).
322 void ReadEnclosure (XmlReader reader
, SyndicationLink link
)
324 link
.RelationshipType
= "enclosure";
326 if (PreserveAttributeExtensions
&& reader
.MoveToFirstAttribute ()) {
328 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
330 if (reader
.NamespaceURI
== String
.Empty
) {
331 switch (reader
.LocalName
) {
333 link
.Uri
= CreateUri (reader
.Value
);
336 link
.MediaType
= reader
.Value
;
339 link
.Length
= XmlConvert
.ToInt64 (reader
.Value
);
343 if (!TryParseAttribute (reader
.LocalName
, reader
.NamespaceURI
, reader
.Value
, link
, Version
))
344 link
.AttributeExtensions
.Add (new XmlQualifiedName (reader
.LocalName
, reader
.NamespaceURI
), reader
.Value
);
345 } while (reader
.MoveToNextAttribute ());
346 reader
.MoveToElement ();
349 // Actually .NET fails to read extension here.
350 if (!reader
.IsEmptyElement
) {
352 for (reader
.MoveToContent (); reader
.NodeType
!= XmlNodeType
.EndElement
; reader
.MoveToContent ()) {
353 if (!TryParseElement (reader
, link
, Version
)) {
354 if (PreserveElementExtensions
)
355 // FIXME: what should be used for maxExtenswionSize
356 LoadElementExtensions (reader
, link
, int.MaxValue
);
362 reader
.Read (); // </enclosure> or <enclosure ... />
365 void ReadLink (XmlReader reader
, SyndicationLink link
)
367 if (PreserveAttributeExtensions
&& reader
.MoveToFirstAttribute ()) {
369 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
371 if (!TryParseAttribute (reader
.LocalName
, reader
.NamespaceURI
, reader
.Value
, link
, Version
))
372 link
.AttributeExtensions
.Add (new XmlQualifiedName (reader
.LocalName
, reader
.NamespaceURI
), reader
.Value
);
373 } while (reader
.MoveToNextAttribute ());
374 reader
.MoveToElement ();
377 if (!reader
.IsEmptyElement
) {
380 for (reader
.MoveToContent (); reader
.NodeType
!= XmlNodeType
.EndElement
; reader
.MoveToContent ()) {
381 if (reader
.IsTextNode ())
383 else if (!TryParseElement (reader
, link
, Version
)) {
384 if (PreserveElementExtensions
)
385 // FIXME: what should be used for maxExtenswionSize
386 LoadElementExtensions (reader
, link
, int.MaxValue
);
392 link
.Uri
= CreateUri (url
);
394 reader
.Read (); // </link> or <link ... />
397 void ReadPerson (XmlReader reader
, SyndicationPerson person
)
399 if (PreserveAttributeExtensions
&& reader
.MoveToFirstAttribute ()) {
401 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
403 if (!TryParseAttribute (reader
.LocalName
, reader
.NamespaceURI
, reader
.Value
, person
, Version
))
404 person
.AttributeExtensions
.Add (new XmlQualifiedName (reader
.LocalName
, reader
.NamespaceURI
), reader
.Value
);
405 } while (reader
.MoveToNextAttribute ());
406 reader
.MoveToElement ();
409 if (!reader
.IsEmptyElement
) {
411 for (reader
.MoveToContent (); reader
.NodeType
!= XmlNodeType
.EndElement
; reader
.MoveToContent ()) {
412 if (reader
.IsTextNode ())
413 person
.Email
+= reader
.Value
;
414 else if (!TryParseElement (reader
, person
, Version
)) {
415 if (PreserveElementExtensions
)
416 // FIXME: what should be used for maxExtenswionSize
417 LoadElementExtensions (reader
, person
, int.MaxValue
);
424 reader
.Read (); // end element or empty element
427 // copied from Atom10ItemFormatter
428 void ReadPersonAtom10 (XmlReader reader
, SyndicationPerson person
)
430 if (reader
.MoveToFirstAttribute ()) {
432 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
434 if (!TryParseAttribute (reader
.LocalName
, reader
.NamespaceURI
, reader
.Value
, person
, Version
) && PreserveAttributeExtensions
)
435 person
.AttributeExtensions
.Add (new XmlQualifiedName (reader
.LocalName
, reader
.NamespaceURI
), reader
.Value
);
436 } while (reader
.MoveToNextAttribute ());
437 reader
.MoveToElement ();
440 if (!reader
.IsEmptyElement
) {
442 for (reader
.MoveToContent (); reader
.NodeType
!= XmlNodeType
.EndElement
; reader
.MoveToContent ()) {
443 if (reader
.NodeType
== XmlNodeType
.Element
&& reader
.NamespaceURI
== AtomNamespace
) {
444 switch (reader
.LocalName
) {
446 person
.Name
= reader
.ReadElementContentAsString ();
449 person
.Uri
= reader
.ReadElementContentAsString ();
452 person
.Email
= reader
.ReadElementContentAsString ();
456 if (!TryParseElement (reader
, person
, Version
)) {
457 if (PreserveElementExtensions
)
458 // FIXME: what should be used for maxExtenswionSize
459 LoadElementExtensions (reader
, person
, int.MaxValue
);
465 reader
.Read (); // end element or empty element
468 void ReadSourceFeed (XmlReader reader
, SyndicationFeed feed
)
470 if (reader
.MoveToFirstAttribute ()) {
472 if (reader
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
474 if (reader
.NamespaceURI
== String
.Empty
) {
475 switch (reader
.LocalName
) {
477 feed
.Links
.Add (new SyndicationLink (CreateUri (reader
.Value
)));
481 } while (reader
.MoveToNextAttribute ());
482 reader
.MoveToElement ();
485 if (!reader
.IsEmptyElement
) {
488 while (reader
.NodeType
!= XmlNodeType
.EndElement
) {
489 if (reader
.IsTextNode ())
490 title
+= reader
.Value
;
492 reader
.MoveToContent ();
494 feed
.Title
= new TextSyndicationContent (title
);
496 reader
.Read (); // </source> or <source ... />
499 Uri
CreateUri (string uri
)
501 return new Uri (uri
, UriKind
.RelativeOrAbsolute
);
506 void WriteXml (XmlWriter writer
, bool writeRoot
)
509 throw new ArgumentNullException ("writer");
511 throw new InvalidOperationException ("Syndication item must be set before writing");
514 writer
.WriteStartElement ("item");
516 if (Item
.BaseUri
!= null)
517 writer
.WriteAttributeString ("xml:base", Item
.BaseUri
.ToString ());
519 WriteAttributeExtensions (writer
, Item
, Version
);
521 if (Item
.Id
!= null) {
522 writer
.WriteStartElement ("guid");
523 writer
.WriteAttributeString ("isPermaLink", "false");
524 writer
.WriteString (Item
.Id
);
525 writer
.WriteEndElement ();
528 if (Item
.Title
!= null) {
529 writer
.WriteStartElement ("title");
530 writer
.WriteString (Item
.Title
.Text
);
531 writer
.WriteEndElement ();
534 foreach (SyndicationPerson author
in Item
.Authors
)
535 if (author
!= null) {
536 writer
.WriteStartElement ("author");
537 WriteAttributeExtensions (writer
, author
, Version
);
538 writer
.WriteString (author
.Email
);
539 WriteElementExtensions (writer
, author
, Version
);
540 writer
.WriteEndElement ();
542 foreach (SyndicationCategory category
in Item
.Categories
)
543 if (category
!= null) {
544 writer
.WriteStartElement ("category");
545 if (category
.Scheme
!= null)
546 writer
.WriteAttributeString ("domain", category
.Scheme
);
547 WriteAttributeExtensions (writer
, category
, Version
);
548 writer
.WriteString (category
.Name
);
549 WriteElementExtensions (writer
, category
, Version
);
550 writer
.WriteEndElement ();
553 if (Item
.Content
!= null) {
554 Item
.Content
.WriteTo (writer
, "description", String
.Empty
);
555 } else if (Item
.Summary
!= null)
556 Item
.Summary
.WriteTo (writer
, "description", String
.Empty
);
557 else if (Item
.Title
== null) { // according to the RSS 2.0 spec, either of title or description must exist.
558 writer
.WriteStartElement ("description");
559 writer
.WriteEndElement ();
562 foreach (SyndicationLink link
in Item
.Links
)
563 switch (link
.RelationshipType
) {
565 writer
.WriteStartElement ("enclosure");
566 if (link
.Uri
!= null)
567 writer
.WriteAttributeString ("uri", link
.Uri
.ToString ());
568 if (link
.Length
!= 0)
569 writer
.WriteAttributeString ("length", XmlConvert
.ToString (link
.Length
));
570 if (link
.MediaType
!= null)
571 writer
.WriteAttributeString ("type", link
.MediaType
);
572 WriteAttributeExtensions (writer
, link
, Version
);
573 WriteElementExtensions (writer
, link
, Version
);
574 writer
.WriteEndElement ();
577 writer
.WriteStartElement ("link");
578 WriteAttributeExtensions (writer
, link
, Version
);
579 writer
.WriteString (link
.Uri
!= null ? link
.Uri
.ToString () : String
.Empty
);
580 WriteElementExtensions (writer
, link
, Version
);
581 writer
.WriteEndElement ();
585 if (Item
.SourceFeed
!= null) {
586 writer
.WriteStartElement ("source");
587 if (Item
.SourceFeed
.Links
.Count
> 0) {
588 Uri u
= Item
.SourceFeed
.Links
[0].Uri
;
589 writer
.WriteAttributeString ("url", u
!= null ? u
.ToString () : String
.Empty
);
591 writer
.WriteString (Item
.SourceFeed
.Title
!= null ? Item
.SourceFeed
.Title
.Text
: String
.Empty
);
592 writer
.WriteEndElement ();
595 if (!Item
.PublishDate
.Equals (default (DateTimeOffset
))) {
596 writer
.WriteStartElement ("pubDate");
597 writer
.WriteString (ToRFC822DateString (Item
.PublishDate
));
598 writer
.WriteEndElement ();
601 if (SerializeExtensionsAsAtom
) {
602 foreach (SyndicationPerson contributor
in Item
.Contributors
) {
603 if (contributor
!= null) {
604 writer
.WriteStartElement ("contributor", AtomNamespace
);
605 WriteAttributeExtensions (writer
, contributor
, Version
);
606 writer
.WriteElementString ("name", AtomNamespace
, contributor
.Name
);
607 writer
.WriteElementString ("uri", AtomNamespace
, contributor
.Uri
);
608 writer
.WriteElementString ("email", AtomNamespace
, contributor
.Email
);
609 WriteElementExtensions (writer
, contributor
, Version
);
610 writer
.WriteEndElement ();
614 if (!Item
.LastUpdatedTime
.Equals (default (DateTimeOffset
))) {
615 writer
.WriteStartElement ("updated", AtomNamespace
);
616 // FIXME: how to handle offset part?
617 writer
.WriteString (XmlConvert
.ToString (Item
.LastUpdatedTime
.DateTime
, XmlDateTimeSerializationMode
.Local
));
618 writer
.WriteEndElement ();
621 if (Item
.Copyright
!= null)
622 Item
.Copyright
.WriteTo (writer
, "rights", AtomNamespace
);
624 if (Item
.Content
!= null)
625 Item
.Content
.WriteTo (writer
, "content", AtomNamespace
);
629 WriteElementExtensions (writer
, Item
, Version
);
632 writer
.WriteEndElement ();
635 // FIXME: DateTimeOffset.ToString() needs another overload.
636 // When it is implemented, just remove ".DateTime" parts below.
637 string ToRFC822DateString (DateTimeOffset date
)
639 switch (date
.DateTime
.Kind
) {
640 case DateTimeKind
.Utc
:
641 return date
.DateTime
.ToString ("ddd, dd MMM yyyy HH:mm:ss 'Z'", DateTimeFormatInfo
.InvariantInfo
);
642 case DateTimeKind
.Local
:
643 StringBuilder sb
= new StringBuilder (date
.DateTime
.ToString ("ddd, dd MMM yyyy HH:mm:ss zzz", DateTimeFormatInfo
.InvariantInfo
));
644 sb
.Remove (sb
.Length
- 3, 1);
645 return sb
.ToString (); // remove ':' from +hh:mm
647 return date
.DateTime
.ToString ("ddd, dd MMM yyyy HH:mm:ss", DateTimeFormatInfo
.InvariantInfo
);
651 string [] rfc822formats
= new string [] {
652 "ddd, dd MMM yyyy HH:mm:ss 'Z'",
653 "ddd, dd MMM yyyy HH:mm:ss zzz",
654 "ddd, dd MMM yyyy HH:mm:ss"};
656 // FIXME: DateTimeOffset is still incomplete. When it is done,
657 // simplify the code.
658 DateTimeOffset
FromRFC822DateString (string s
)
660 return XmlConvert
.ToDateTimeOffset (s
, rfc822formats
);