001/**
002 * Copyright 2011 Bill Brown
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package com.colorfulsoftware.rss;
017
018import java.io.Serializable;
019import java.util.LinkedList;
020import java.util.List;
021
022/**
023 * <p>
024 * The &lt;rss> element.
025 * </p>
026 * <p>
027 * From the <a href="http://cyber.law.harvard.edu/rss/rss.html">RSS 2.0
028 * specification</a>...
029 * </p>
030 * <p>
031 * At the top level, a RSS document is a &lt;rss> element, with a mandatory
032 * attribute called version, that specifies the version of RSS that the document
033 * conforms to. If it conforms to this specification, the version attribute must
034 * be 2.0.
035 * </p>
036 * 
037 * <p>
038 * Subordinate to the &lt;rss> element is a single &lt;channel> element, which
039 * contains information about the channel (metadata) and its contents. RSS
040 * originated in 1999, and has strived to be a simple, easy to understand
041 * format, with relatively modest goals. After it became a popular format,
042 * developers wanted to extend it using modules defined in namespaces, as
043 * specified by the W3C.
044 * </p>
045 * 
046 * <p>
047 * RSS 2.0 adds that capability, following a simple rule. A RSS feed may contain
048 * elements not described on this page, only if those elements are defined in a
049 * namespace.
050 * </p>
051 * 
052 * <p>
053 * The elements defined in this document are not themselves members of a
054 * namespace, so that RSS 2.0 can remain compatible with previous versions in
055 * the following sense - - a version 0.91 or 0.92 file is also a valid 2.0 file.
056 * If the elements of RSS 2.0 were in a namespace, this constraint would break,
057 * a version 0.9x file would not be a valid 2.0 file.
058 * </p>
059 * 
060 * <p>
061 * The version="2.0" attribute of the &lt;rss> element is automatically provided
062 * in this implementation.
063 * </p>
064 * 
065 * @author Bill Brown
066 * 
067 */
068public class RSS implements Serializable {
069
070        /**
071         * 
072         */
073        private static final long serialVersionUID = -2952021194132587749L;
074
075        private final Channel channel;
076        private final List<Attribute> attributes;
077        private final List<Extension> extensions;
078        private List<String> unboundPrefixes;
079
080        RSS(Channel channel, List<Attribute> attributes, List<Extension> extensions)
081                        throws RSSpectException {
082                if (channel == null) {
083                        throw new RSSpectException(
084                                        "rss elements MUST contain a channel element.");
085                }
086
087                this.channel = new Channel(channel);
088
089                if (attributes == null) {
090                        throw new RSSpectException(
091                                        "RSS elements must contain a version attribute.");
092                } else {
093                        this.attributes = new LinkedList<Attribute>();
094
095                        boolean containsVersion = false;
096
097                        for (Attribute attr : attributes) {
098                                this.attributes.add(new Attribute(attr));
099                                if (attr.getName().equals("version")) {
100                                        containsVersion = true;
101                                }
102                        }
103
104                        if (!containsVersion) {
105                                throw new RSSpectException(
106                                                "RSS elements must contain a version attribute.");
107                        }
108                }
109
110                // check that all extension prefixes are bound to a namespace
111                this.unboundPrefixes = new LinkedList<String>();
112
113                if (this.channel.getUnboundPrefixes() != null) {
114                        for (String unboundPrefix : this.channel.getUnboundPrefixes()) {
115                                if (getAttribute("xmlns:" + unboundPrefix) == null) {
116                                        this.unboundPrefixes.add(unboundPrefix);
117                                }
118                        }
119                }
120
121                if (extensions == null) {
122                        this.extensions = null;
123                } else {
124                        this.extensions = new LinkedList<Extension>();
125                        for (Extension extension : extensions) {
126                                // check that the extension prefix is bound to a namespace
127                                String namespacePrefix = extension.getNamespacePrefix();
128                                if (namespacePrefix != null) {
129                                        if (getAttribute("xmlns:" + namespacePrefix) == null) {
130                                                this.unboundPrefixes.add(namespacePrefix);
131                                        }
132                                }
133                                this.extensions.add(new Extension(extension));
134                        }
135                }
136
137                // if there are any unbound prefixes, throw an exception
138                if (this.unboundPrefixes.size() > 0) {
139                        StringBuilder sb = new StringBuilder();
140                        for (String namePrefix : this.unboundPrefixes) {
141                                sb.append(namePrefix + " ");
142                        }
143                        throw new RSSpectException(
144                                        "the following extension prefix(es) ( "
145                                                        + sb
146                                                        + ") are not bound to a namespace declaration. See http://www.w3.org/TR/1999/REC-xml-names-19990114/#ns-decl.");
147                }
148        }
149
150        /**
151         * @return the channel object.
152         */
153        public Channel getChannel() {
154                return new Channel(channel);
155        }
156
157        /**
158         * 
159         * @return the attribute list.
160         */
161        public List<Attribute> getAttributes() {
162
163                List<Attribute> attrsCopy = new LinkedList<Attribute>();
164                for (Attribute attr : this.attributes) {
165                        attrsCopy.add(new Attribute(attr));
166
167                }
168                return attrsCopy;
169        }
170
171        /**
172         * 
173         * @return the extensions for this entry.
174         */
175        public List<Extension> getExtensions() {
176                if (extensions == null) {
177                        return null;
178                }
179                List<Extension> extsCopy = new LinkedList<Extension>();
180                for (Extension extension : this.extensions) {
181                        extsCopy.add(new Extension(extension));
182                }
183                return extsCopy;
184        }
185
186        /**
187         * @param attrName
188         *            the name of the attribute to get.
189         * @return the Attribute object if attrName matches or null if not found.
190         */
191        public Attribute getAttribute(String attrName) {
192                for (Attribute attribute : this.attributes) {
193                        if (attribute.getName().equals(attrName)) {
194                                return new Attribute(attribute);
195
196                        }
197                }
198                return null;
199        }
200
201        /**
202         * @param extName
203         *            the element name of the extension. eg. "atom:link" or
204         *            "someExtension"
205         * @return the extension matching the element or null if not found.
206         */
207        public Extension getExtension(String extName) {
208                if (this.extensions != null) {
209                        for (Extension extension : this.extensions) {
210                                if (extension.getElementName().equals(extName)) {
211                                        return new Extension(extension);
212                                }
213                        }
214                }
215                return null;
216        }
217
218        /**
219         * Shows the contents of the &lt;rss> element.
220         */
221        @Override
222        public String toString() {
223                StringBuilder sb = new StringBuilder("<rss");
224                for (Attribute attribute : attributes) {
225                        sb.append(attribute);
226                }
227                // close the parent element
228                sb.append(">");
229
230                sb.append(channel);
231
232                if (extensions != null) {
233                        for (Extension extension : extensions) {
234                                sb.append(extension);
235                        }
236                }
237
238                sb.append("</rss>");
239                return sb.toString();
240        }
241
242        @Override
243        public boolean equals(Object obj) {
244                if (obj == this) {
245                        return true;
246                }
247                if (!(obj instanceof RSS)) {
248                        return false;
249                }
250                return this.toString().equals(obj.toString());
251        }
252        
253        @Override public int hashCode() {
254                return toString().hashCode();
255        }
256}