RSSHandler.java (9343B)
1 /* 2 * Copyright (C) 2010 A. Horn 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.mcsoxford.rss; 18 19 /** 20 * Internal SAX handler to efficiently parse RSS feeds. Only a single thread 21 * must use this SAX handler. 22 * 23 * @author Mr Horn 24 */ 25 class RSSHandler extends org.xml.sax.helpers.DefaultHandler { 26 27 /** 28 * Constant for XML element name which identifies RSS items. 29 */ 30 private static final String RSS_ITEM = "item"; 31 32 /** 33 * Constant symbol table to ensure efficient treatment of handler states. 34 */ 35 private final java.util.Map<String, Setter> setters; 36 37 /** 38 * Reference is never {@code null}. Visibility must be package-private to 39 * ensure efficiency of inner classes. 40 */ 41 final RSSFeed feed = new RSSFeed(); 42 43 /** 44 * Reference is {@code null} unless started to parse <item> element. 45 * Visibility must be package-private to ensure efficiency of inner classes. 46 */ 47 RSSItem item; 48 49 /** 50 * If not {@code null}, then buffer the characters inside an XML text element. 51 */ 52 private StringBuilder buffer; 53 54 /** 55 * Dispatcher to set either {@link #feed} or {@link #item} fields. 56 */ 57 private Setter setter; 58 59 /** 60 * Interface to store information about RSS elements. 61 */ 62 private static interface Setter {} 63 64 /** 65 * Closure to change fields in POJOs which store RSS content. 66 */ 67 private static interface ContentSetter extends Setter { 68 69 /** 70 * Set the field of an object which represents an RSS element. 71 */ 72 void set(String value); 73 74 } 75 76 /** 77 * Closure to change fields in POJOs which store information 78 * about RSS elements which have only attributes. 79 */ 80 private static interface AttributeSetter extends Setter { 81 82 /** 83 * Set the XML attributes. 84 */ 85 void set(org.xml.sax.Attributes attributes); 86 87 } 88 89 /** 90 * Setter for RSS <title> elements inside a <channel> or an 91 * <item> element. The title of the RSS feed is set only if 92 * {@link #item} is {@code null}. Otherwise, the title of the RSS 93 * {@link #item} is set. 94 */ 95 private final Setter SET_TITLE = new ContentSetter() { 96 @Override 97 public void set(String title) { 98 if (item == null) { 99 feed.setTitle(title); 100 } else { 101 item.setTitle(title); 102 } 103 } 104 }; 105 106 /** 107 * Setter for RSS <description> elements inside a <channel> or an 108 * <item> element. The title of the RSS feed is set only if 109 * {@link #item} is {@code null}. Otherwise, the title of the RSS 110 * {@link #item} is set. 111 */ 112 private final Setter SET_DESCRIPTION = new ContentSetter() { 113 @Override 114 public void set(String description) { 115 if (item == null) { 116 feed.setDescription(description); 117 } else { 118 item.setDescription(description); 119 } 120 } 121 }; 122 123 /** 124 * Setter for an RSS <content:encoded> element inside an <item> 125 * element. 126 */ 127 private final Setter SET_CONTENT = new ContentSetter() { 128 @Override 129 public void set(String content) { 130 if (item != null) { 131 item.setFullContent(content); 132 } 133 } 134 }; 135 136 /** 137 * Setter for RSS <link> elements inside a <channel> or an 138 * <item> element. The title of the RSS feed is set only if 139 * {@link #item} is {@code null}. Otherwise, the title of the RSS 140 * {@link #item} is set. 141 */ 142 private final Setter SET_LINK = new ContentSetter() { 143 @Override 144 public void set(String link) { 145 final android.net.Uri uri = android.net.Uri.parse(link); 146 if (item == null) { 147 feed.setLink(uri); 148 } else { 149 item.setLink(uri); 150 } 151 } 152 }; 153 154 /** 155 * Setter for RSS <pubDate> elements inside a <channel> or an 156 * <item> element. The title of the RSS feed is set only if 157 * {@link #item} is {@code null}. Otherwise, the title of the RSS 158 * {@link #item} is set. 159 */ 160 private final Setter SET_PUBDATE = new ContentSetter() { 161 @Override 162 public void set(String pubDate) { 163 final java.util.Date date = Dates.parseRfc822(pubDate); 164 if (item == null) { 165 feed.setPubDate(date); 166 } else { 167 item.setPubDate(date); 168 } 169 } 170 }; 171 172 /** 173 * Setter for one or multiple RSS <category> elements inside a 174 * <channel> or an <item> element. The title of the RSS feed is 175 * set only if {@link #item} is {@code null}. Otherwise, the title of the RSS 176 * {@link #item} is set. 177 */ 178 private final Setter ADD_CATEGORY = new ContentSetter() { 179 180 @Override 181 public void set(String category) { 182 if (item == null) { 183 feed.addCategory(category); 184 } else { 185 item.addCategory(category); 186 } 187 } 188 }; 189 190 /** 191 * Setter for one or multiple RSS <media:thumbnail> elements inside an 192 * <item> element. The thumbnail element has only attributes. Both its 193 * height and width are optional. Invalid elements are ignored. 194 */ 195 private final Setter ADD_MEDIA_THUMBNAIL = new AttributeSetter() { 196 197 private static final String MEDIA_THUMBNAIL_HEIGHT = "height"; 198 private static final String MEDIA_THUMBNAIL_WIDTH = "width"; 199 private static final String MEDIA_THUMBNAIL_URL = "url"; 200 private static final int DEFAULT_DIMENSION = -1; 201 202 @Override 203 public void set(org.xml.sax.Attributes attributes) { 204 if (item == null) { 205 // ignore invalid media:thumbnail elements which are not inside item 206 // elements 207 return; 208 } 209 210 final int height = MediaAttributes.intValue(attributes, MEDIA_THUMBNAIL_HEIGHT, DEFAULT_DIMENSION); 211 final int width = MediaAttributes.intValue(attributes, MEDIA_THUMBNAIL_WIDTH, DEFAULT_DIMENSION); 212 final String url = MediaAttributes.stringValue(attributes, MEDIA_THUMBNAIL_URL); 213 214 if (url == null) { 215 // ignore invalid media:thumbnail elements which have no URL. 216 return; 217 } 218 219 item.addThumbnail(new MediaThumbnail(android.net.Uri.parse(url), height, width)); 220 } 221 222 }; 223 224 /** 225 * Use configuration to optimize initial capacities of collections 226 */ 227 private final RSSConfig config; 228 229 /** 230 * Instantiate a SAX handler which can parse a subset of RSS 2.0 feeds. 231 * 232 * @param config configuration for the initial capacities of collections 233 */ 234 RSSHandler(RSSConfig config) { 235 this.config = config; 236 237 // initialize dispatchers to manage the state of the SAX handler 238 setters = new java.util.HashMap<String, Setter>(/* 2^3 */8); 239 setters.put("title", SET_TITLE); 240 setters.put("description", SET_DESCRIPTION); 241 setters.put("content:encoded", SET_CONTENT); 242 setters.put("link", SET_LINK); 243 setters.put("category", ADD_CATEGORY); 244 setters.put("pubDate", SET_PUBDATE); 245 setters.put("media:thumbnail", ADD_MEDIA_THUMBNAIL); 246 } 247 248 /** 249 * Returns the RSS feed after this SAX handler has processed the XML document. 250 */ 251 RSSFeed feed() { 252 return feed; 253 } 254 255 /** 256 * Identify the appropriate dispatcher which should be used to store XML data 257 * in a POJO. Unsupported RSS 2.0 elements are currently ignored. 258 */ 259 @Override 260 public void startElement(String nsURI, String localName, String qname, 261 org.xml.sax.Attributes attributes) { 262 // Lookup dispatcher in hash table 263 setter = setters.get(qname); 264 if (setter == null) { 265 if (RSS_ITEM.equals(qname)) { 266 item = new RSSItem(config.categoryAvg, config.thumbnailAvg); 267 } 268 } else if (setter instanceof AttributeSetter) { 269 ((AttributeSetter) setter).set(attributes); 270 } else { 271 // Buffer supported RSS content data 272 buffer = new StringBuilder(); 273 } 274 } 275 276 @Override 277 public void endElement(String nsURI, String localName, String qname) { 278 if (isBuffering()) { 279 // set field of an RSS feed or RSS item 280 ((ContentSetter) setter).set(buffer.toString()); 281 282 // clear buffer 283 buffer = null; 284 } else if (RSS_ITEM.equals(qname)) { 285 feed.addItem(item); 286 287 // (re)enter <channel> scope 288 item = null; 289 } 290 } 291 292 @Override 293 public void characters(char ch[], int start, int length) { 294 if (isBuffering()) { 295 buffer.append(ch, start, length); 296 } 297 } 298 299 /** 300 * Determines if the SAX parser is ready to receive data inside an XML element 301 * such as <title> or <description>. 302 * 303 * @return boolean {@code true} if the SAX handler parses data inside an XML 304 * element, {@code false} otherwise 305 */ 306 boolean isBuffering() { 307 return buffer != null && setter != null; 308 } 309 310 } 311