/* RssWriter.cs * ============ * * RSS.NET (http://rss-net.sf.net/) * Copyright © 2002, 2003 George Tsiokos. All Rights Reserved. * * RSS 2.0 (http://blogs.law.harvard.edu/tech/rss) * RSS 2.0 is offered by the Berkman Center for Internet & Society at * Harvard Law School under the terms of the Attribution/Share Alike * Creative Commons license. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ using System; using System.Xml; using System.Text; using System.IO; namespace Rss { /// Writes an RSS XML file. /// Represents a writer that provides a fast, non-cached, forward-only way of generating streams or files containing RSS XML data that conforms to the W3C Extensible Markup Language (XML) 1.0 and the Namespaces in XML recommendations. public class RssWriter { private XmlTextWriter writer = null; // functional var private bool wroteStartDocument = false; private bool wroteChannel = false; // prefrences private RssVersion rssVersion = RssVersion.RSS20; private Formatting xmlFormat = Formatting.Indented; private int xmlIndentation = 2; // constants private const string DateTimeFormatString = "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'"; // modules private RssModuleCollection _rssModules = new RssModuleCollection(); #region Constructors /// Creates an instance of the RssWriter class using the specified TextWriter. /// specified TextWriter public RssWriter(TextWriter textWriter) { writer = new XmlTextWriter(textWriter); } /// Creates an instance of the RssWriter class using the specified Stream and Encoding. /// The encoding is not supported or the stream cannot be written to. /// Stream to output to /// The encoding to use. If encoding is (null c#, Nothing vb) it writes out the stream as UTF-8. public RssWriter(Stream stream, Encoding encoding) { writer = new XmlTextWriter(stream, encoding); } /// Creates an instance of the RssWriter class using the specified Stream. /// The encoding is ISO-8859-1. /// The Stream cannot be written to. /// specified Stream public RssWriter(Stream stream) { writer = new XmlTextWriter(stream, System.Text.Encoding.GetEncoding("ISO-8859-1")); } /// Creates an instance of the RssWriter class using the specified file and Encoding. /// The encoding is not supported; the filename is empty, contains only white space, or contains one or more invalid characters. /// Access is denied. /// The filename is a (null c#, Nothing vb) reference. /// The directory to write to is not found. /// The filename includes an incorrect or invalid syntax for file name, directory name, or volume label syntax. /// The caller does not have the required permission. /// specified file (including path) If the file exists, it will be truncated with the new content. /// specified Encoding public RssWriter(string fileName, Encoding encoding) { writer = new XmlTextWriter(fileName, encoding); } /// Creates an instance of the RssWriter class using the specified file. /// The encoding is ISO-8859-1. /// The filename is empty, contains only white space, or contains one or more invalid characters. /// Access is denied. /// The filename is a (null c#, Nothing vb) reference. /// The directory to write to is not found. /// The filename includes an incorrect or invalid syntax for file name, directory name, or volume label syntax. /// The caller does not have the required permission. /// specified file (including path) If the file exists, it will be truncated with the new content. public RssWriter(string fileName) { writer = new XmlTextWriter(fileName, System.Text.Encoding.GetEncoding("ISO-8859-1")); } #endregion /// Writes the begining data to the RSS file /// This routine is called from the WriteChannel and WriteItem subs /// RDF Site Summary (RSS) 1.0 is not currently supported. private void BeginDocument() { if (!wroteStartDocument) { if (rssVersion == RssVersion.Empty) rssVersion = RssVersion.RSS20; writer.Formatting = xmlFormat; writer.Indentation = xmlIndentation; writer.WriteStartDocument(); if (rssVersion != RssVersion.RSS20) writer.WriteComment("Generated by RSS.NET: http://rss-net.sf.net"); //exc: The xml:space or xml:lang attribute value is invalid. switch (rssVersion) { case RssVersion.RSS090: // writer.WriteStartElement("RDF", "rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); break; case RssVersion.RSS091: writer.WriteStartElement("rss"); writer.WriteDocType("rss", "-//Netscape Communications//DTD RSS 0.91//EN", "http://my.netscape.com/publish/formats/rss-0.91.dtd", null); writer.WriteAttributeString("version", "0.91"); break; case RssVersion.RSS092: writer.WriteStartElement("rss"); writer.WriteAttributeString("version", "0.92"); break; case RssVersion.RSS10: throw new NotSupportedException("RDF Site Summary (RSS) 1.0 is not currently supported."); case RssVersion.RSS20: writer.WriteStartElement("rss"); writer.WriteAttributeString("version", "2.0"); // RSS Modules foreach(RssModule rssModule in this._rssModules) { WriteAttribute("xmlns:" + rssModule.NamespacePrefix, rssModule.NamespaceURL.ToString(), true); } break; } wroteStartDocument = true; } } private void writeChannel(RssChannel channel) { if (writer == null) throw new InvalidOperationException("RssWriter has been closed, and can not be written to."); if (channel == null) throw new ArgumentNullException("Channel must be instanciated with data to be written."); if (wroteChannel) writer.WriteEndElement(); else wroteChannel = true; BeginDocument(); writer.WriteStartElement("channel"); WriteElement("title", channel.Title, true); WriteElement("description", channel.Description, true); WriteElement("link", channel.Link, true); if (channel.Image != null) { writer.WriteStartElement("image"); WriteElement("title", channel.Image.Title, true); WriteElement("url", channel.Image.Url, true); WriteElement("link", channel.Image.Link, true); switch (rssVersion) { case RssVersion.RSS091: case RssVersion.RSS092: case RssVersion.RSS20: WriteElement("description", channel.Image.Description, false); WriteElement("width", channel.Image.Width, false); WriteElement("height", channel.Image.Height, false); break; } writer.WriteEndElement(); } switch (rssVersion) { case RssVersion.RSS091: case RssVersion.RSS092: case RssVersion.RSS20: WriteElement("language", channel.Language, rssVersion == RssVersion.RSS091); WriteElement("copyright", channel.Copyright, false); WriteElement("managingEditor", channel.ManagingEditor, false); WriteElement("webMaster", channel.WebMaster, false); WriteElement("pubDate", channel.PubDate, false); WriteElement("lastBuildDate", channel.LastBuildDate, false); if (channel.Docs != RssDefault.String) WriteElement("docs", channel.Docs, false); else switch (rssVersion) { case RssVersion.RSS091: WriteElement("docs", "http://my.netscape.com/publish/formats/rss-spec-0.91.html", false); break; case RssVersion.RSS092: WriteElement("docs", "http://backend.userland.com/rss092", false); break; case RssVersion.RSS20: WriteElement("docs", "http://backend.userland.com/rss", false); break; } WriteElement("rating", channel.Rating, false); string[] Days = {"monday","tuesday","wednesday","thursday","friday","saturday","sunday"}; for (int i = 0; i <= 6; i++) if (channel.SkipDays[i]) { writer.WriteStartElement("skipDays"); for (int i2 = 0; i2 <= 6; i2++) if (channel.SkipDays[i2]) WriteElement("day", Days[i2], false); writer.WriteEndElement(); break; } for (int i = 0; i <= 23; i++) if (channel.SkipHours[i]) { writer.WriteStartElement("skipHours"); for (int i2 = 0; i2<= 23; i2++) if (channel.SkipHours[i2]) WriteElement("hour", i2+1, false); writer.WriteEndElement(); break; } break; } switch (rssVersion) { case RssVersion.RSS092: case RssVersion.RSS20: if (channel.Categories != null) foreach(RssCategory category in channel.Categories) if (category.Name != RssDefault.String) { writer.WriteStartElement("category"); WriteAttribute("domain", category.Domain, false); writer.WriteString(category.Name); writer.WriteEndElement(); } if (channel.Cloud != null) { writer.WriteStartElement("cloud"); WriteElement("domain", channel.Cloud.Domain, false); WriteElement("port", channel.Cloud.Port, false); WriteElement("path", channel.Cloud.Path, false); WriteElement("registerProcedure", channel.Cloud.RegisterProcedure, false); if (channel.Cloud.Protocol != RssCloudProtocol.Empty) WriteElement("Protocol", channel.Cloud.Protocol, false); writer.WriteEndElement(); } break; } if (rssVersion == RssVersion.RSS20) { if (channel.Generator != RssDefault.String) WriteElement("generator", channel.Generator, false); else WriteElement("generator", "RSS.NET: http://www.rssdotnet.com/", false); WriteElement("ttl", channel.TimeToLive, false); // RSS Modules foreach(RssModule rssModule in this._rssModules) { if(rssModule.IsBoundTo(channel.GetHashCode())) { foreach(RssModuleItem rssModuleItem in rssModule.ChannelExtensions) { if(rssModuleItem.SubElements.Count == 0) WriteElement(rssModule.NamespacePrefix + ":" + rssModuleItem.Name, rssModuleItem.Text, rssModuleItem.IsRequired); else writeSubElements(rssModuleItem.SubElements, rssModule.NamespacePrefix); } } } } if (channel.TextInput != null) { writer.WriteStartElement("textinput"); WriteElement("title", channel.TextInput.Title, true); WriteElement("description", channel.TextInput.Description, true); WriteElement("name", channel.TextInput.Name, true); WriteElement("link", channel.TextInput.Link, true); writer.WriteEndElement(); } foreach (RssItem item in channel.Items) { writeItem(item, channel.GetHashCode()); } writer.Flush(); } private void writeItem(RssItem item, int channelHashCode) { if (writer == null) throw new InvalidOperationException("RssWriter has been closed, and can not be written to."); if (item == null) throw new ArgumentNullException("Item must be instanciated with data to be written."); if (!wroteChannel) throw new InvalidOperationException("Channel must be written first, before writing an item."); BeginDocument(); writer.WriteStartElement("item"); switch (rssVersion) { case RssVersion.RSS090: case RssVersion.RSS10: case RssVersion.RSS091: WriteElement("title", item.Title, true); WriteElement("description", item.Description, false); WriteElement("link", item.Link, true); break; case RssVersion.RSS20: if ((item.Title == RssDefault.String) && (item.Description == RssDefault.String)) throw new ArgumentException("item title and description cannot be null"); goto case RssVersion.RSS092; case RssVersion.RSS092: WriteElement("title", item.Title, false); WriteElement("description", item.Description, false); WriteElement("link", item.Link, false); if (item.Source != null) { writer.WriteStartElement("source"); WriteAttribute("url", item.Source.Url, true); writer.WriteString(item.Source.Name); writer.WriteEndElement(); } if (item.Enclosure != null) { writer.WriteStartElement("enclosure"); WriteAttribute("url", item.Enclosure.Url, true); WriteAttribute("length", item.Enclosure.Length, true); WriteAttribute("type", item.Enclosure.Type, true); writer.WriteEndElement(); } foreach(RssCategory category in item.Categories) if (category.Name != RssDefault.String) { writer.WriteStartElement("category"); WriteAttribute("domain", category.Domain, false); writer.WriteString(category.Name); writer.WriteEndElement(); } break; } if (rssVersion == RssVersion.RSS20) { WriteElement("author", item.Author, false); WriteElement("comments", item.Comments, false); if ((item.Guid != null) && (item.Guid.Name != RssDefault.String)) { writer.WriteStartElement("guid"); try { WriteAttribute("isPermaLink", (bool)item.Guid.PermaLink, false); } catch {} writer.WriteString(item.Guid.Name); writer.WriteEndElement(); } WriteElement("pubDate", item.PubDate, false); foreach(RssModule rssModule in this._rssModules) { if(rssModule.IsBoundTo(channelHashCode)) { foreach(RssModuleItemCollection rssModuleItemCollection in rssModule.ItemExtensions) { if(rssModuleItemCollection.IsBoundTo(item.GetHashCode())) writeSubElements(rssModuleItemCollection, rssModule.NamespacePrefix); } } } } writer.WriteEndElement(); writer.Flush(); } /// Closes instance of RssWriter. /// Writes end elements, and releases connections /// Occurs if the RssWriter is already closed or the caller is attempting to close before writing a channel. public void Close() { if (writer == null) throw new InvalidOperationException("RssWriter has been closed, and can not be closed again."); if (!wroteChannel) throw new InvalidOperationException("Can't close RssWriter without first writing a channel."); else writer.WriteEndElement(); // writer.WriteEndElement(); // or writer.Close(); writer = null; } /// Gets or sets the RSS version to write. /// Can't change version number after data has been written. public RssVersion Version { get { return rssVersion; } set { if(wroteStartDocument) throw new InvalidOperationException("Can't change version number after data has been written."); else rssVersion = value; } } /// Gets or sets the of the XML output. /// Can't change XML formatting after data has been written. public Formatting XmlFormat { get { return xmlFormat; } set { if(wroteStartDocument) throw new InvalidOperationException("Can't change XML formatting after data has been written."); else xmlFormat = value; } } /// Gets or sets how indentation to write for each level in the hierarchy when XmlFormat is set to /// Can't change XML formatting after data has been written. /// Setting this property to a negative value. public int XmlIndentation { get { return xmlIndentation; } set { if(wroteStartDocument) throw new InvalidOperationException("Can't change XML indentation after data has been written."); else if (value < 0) throw new ArgumentException("Setting this property to a negative value."); else xmlIndentation = value; } } /// Writes an RSS channel /// RssWriter has been closed, and can not be written to. /// Channel must be instanciated with data, before calling Write. /// RSS channel to write public void Write(RssChannel channel) { writeChannel(channel); } /// Writes an RSS item /// Either the RssWriter has already been closed, or the caller is attempting to write an RSS item before an RSS channel. /// Item must be instanciated with data, before calling Write. /// RSS item to write public void Write(RssItem item) { // NOTE: Standalone items cannot adhere to modules, hence -1 is passed. This may not be the case, however, no examples have been seen where this is legal. writeItem(item, -1); } /// RSS modules public RssModuleCollection Modules { get { return this._rssModules; } set { this._rssModules = value; } } #region WriteElement /// Writes an element with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteElement(string localName, DateTime input, bool required) { if (input != RssDefault.DateTime) writer.WriteElementString(localName, XmlConvert.ToString(input,DateTimeFormatString)); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an element with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteElement(string localName, int input, bool required) { if (input != RssDefault.Int) writer.WriteElementString(localName, XmlConvert.ToString(input)); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an element with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteElement(string localName, string input, bool required) { if (input != RssDefault.String) writer.WriteElementString(localName, input); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an element with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteElement(string localName, Uri input, bool required) { if (input != RssDefault.Uri) writer.WriteElementString(localName, input.ToString()); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an element with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteElement(string localName, object input, bool required) { if (input != null) writer.WriteElementString(localName, input.ToString()); else if (required) throw new ArgumentException(localName + " can not be null."); } #endregion #region WriteAttribute /// Writes an attribute with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteAttribute(string localName, DateTime input, bool required) { if (input != RssDefault.DateTime) writer.WriteAttributeString(localName, XmlConvert.ToString(input,DateTimeFormatString)); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an attribute with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteAttribute(string localName, int input, bool required) { if (input != RssDefault.Int) writer.WriteAttributeString(localName, XmlConvert.ToString(input)); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an attribute with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteAttribute(string localName, string input, bool required) { if (input != RssDefault.String) writer.WriteAttributeString(localName, input); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an attribute with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteAttribute(string localName, Uri input, bool required) { if (input != RssDefault.Uri) writer.WriteAttributeString(localName, input.ToString()); else if (required) throw new ArgumentException(localName + " can not be null."); } /// Writes an attribute with the specified local name and value /// the localname of the element /// the value of the element /// boolean that determines if input cannot be null private void WriteAttribute(string localName, object input, bool required) { if (input != null) writer.WriteAttributeString(localName, input.ToString()); else if (required) throw new ArgumentException(localName + " can not be null."); } #endregion #region WriteSubElements private void writeSubElements(RssModuleItemCollection items, string NamespacePrefix) { foreach(RssModuleItem rssModuleItem in items) { if(rssModuleItem.SubElements.Count == 0) WriteElement(NamespacePrefix + ":" + rssModuleItem.Name, rssModuleItem.Text, rssModuleItem.IsRequired); else { writer.WriteStartElement(NamespacePrefix + ":" + rssModuleItem.Name); writeSubElements(rssModuleItem.SubElements, NamespacePrefix); writer.WriteEndElement(); } } } #endregion } }