/* * Copyright (C) 2010 A. Horn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mcsoxford.rss; /** * Internal SAX handler to efficiently parse RSS feeds. Only a single thread * must use this SAX handler. * * @author Mr Horn */ class RSSHandler extends org.xml.sax.helpers.DefaultHandler { /** * Constant for XML element name which identifies RSS items. */ private static final String RSS_ITEM = "item"; /** * Constant symbol table to ensure efficient treatment of handler states. */ private final java.util.Map<String, Setter> setters; /** * Reference is never {@code null}. Visibility must be package-private to * ensure efficiency of inner classes. */ final RSSFeed feed = new RSSFeed(); /** * Reference is {@code null} unless started to parse <item> element. * Visibility must be package-private to ensure efficiency of inner classes. */ RSSItem item; /** * If not {@code null}, then buffer the characters inside an XML text element. */ private StringBuilder buffer; /** * Dispatcher to set either {@link #feed} or {@link #item} fields. */ private Setter setter; /** * Interface to store information about RSS elements. */ private static interface Setter {} /** * Closure to change fields in POJOs which store RSS content. */ private static interface ContentSetter extends Setter { /** * Set the field of an object which represents an RSS element. */ void set(String value); } /** * Closure to change fields in POJOs which store information * about RSS elements which have only attributes. */ private static interface AttributeSetter extends Setter { /** * Set the XML attributes. */ void set(org.xml.sax.Attributes attributes); } /** * Setter for RSS <title> elements inside a <channel> or an * <item> element. The title of the RSS feed is set only if * {@link #item} is {@code null}. Otherwise, the title of the RSS * {@link #item} is set. */ private final Setter SET_TITLE = new ContentSetter() { @Override public void set(String title) { if (item == null) { feed.setTitle(title); } else { item.setTitle(title); } } }; /** * Setter for RSS <description> elements inside a <channel> or an * <item> element. The title of the RSS feed is set only if * {@link #item} is {@code null}. Otherwise, the title of the RSS * {@link #item} is set. */ private final Setter SET_DESCRIPTION = new ContentSetter() { @Override public void set(String description) { if (item == null) { feed.setDescription(description); } else { item.setDescription(description); } } }; /** * Setter for an RSS <content:encoded> element inside an <item> * element. */ private final Setter SET_CONTENT = new ContentSetter() { @Override public void set(String content) { if (item != null) { item.setContent(content); } } }; /** * Setter for RSS <link> elements inside a <channel> or an * <item> element. The title of the RSS feed is set only if * {@link #item} is {@code null}. Otherwise, the title of the RSS * {@link #item} is set. */ private final Setter SET_LINK = new ContentSetter() { @Override public void set(String link) { final android.net.Uri uri = android.net.Uri.parse(link); if (item == null) { feed.setLink(uri); } else { item.setLink(uri); } } }; /** * Setter for RSS <pubDate> elements inside a <channel> or an * <item> element. The title of the RSS feed is set only if * {@link #item} is {@code null}. Otherwise, the title of the RSS * {@link #item} is set. */ private final Setter SET_PUBDATE = new ContentSetter() { @Override public void set(String pubDate) { final java.util.Date date = Dates.parseRfc822(pubDate); if (item == null) { feed.setPubDate(date); } else { item.setPubDate(date); } } }; /** * Setter for one or multiple RSS <category> elements inside a * <channel> or an <item> element. The title of the RSS feed is * set only if {@link #item} is {@code null}. Otherwise, the title of the RSS * {@link #item} is set. */ private final Setter ADD_CATEGORY = new ContentSetter() { @Override public void set(String category) { if (item == null) { feed.addCategory(category); } else { item.addCategory(category); } } }; /** * Setter for one or multiple RSS <media:thumbnail> elements inside an * <item> element. The thumbnail element has only attributes. Both its * height and width are optional. Invalid elements are ignored. */ private final Setter ADD_MEDIA_THUMBNAIL = new AttributeSetter() { private static final String MEDIA_THUMBNAIL_HEIGHT = "height"; private static final String MEDIA_THUMBNAIL_WIDTH = "width"; private static final String MEDIA_THUMBNAIL_URL = "url"; private static final int DEFAULT_DIMENSION = -1; @Override public void set(org.xml.sax.Attributes attributes) { if (item == null) { // ignore invalid media:thumbnail elements which are not inside item // elements return; } final int height = MediaAttributes.intValue(attributes, MEDIA_THUMBNAIL_HEIGHT, DEFAULT_DIMENSION); final int width = MediaAttributes.intValue(attributes, MEDIA_THUMBNAIL_WIDTH, DEFAULT_DIMENSION); final String url = MediaAttributes.stringValue(attributes, MEDIA_THUMBNAIL_URL); if (url == null) { // ignore invalid media:thumbnail elements which have no URL. return; } item.addThumbnail(new MediaThumbnail(android.net.Uri.parse(url), height, width)); } }; /** * Use configuration to optimize initial capacities of collections */ private final RSSConfig config; /** * Instantiate a SAX handler which can parse a subset of RSS 2.0 feeds. * * @param config configuration for the initial capacities of collections */ RSSHandler(RSSConfig config) { this.config = config; // initialize dispatchers to manage the state of the SAX handler setters = new java.util.HashMap<String, Setter>(/* 2^3 */8); setters.put("title", SET_TITLE); setters.put("description", SET_DESCRIPTION); setters.put("content:encoded", SET_CONTENT); setters.put("link", SET_LINK); setters.put("category", ADD_CATEGORY); setters.put("pubDate", SET_PUBDATE); setters.put("media:thumbnail", ADD_MEDIA_THUMBNAIL); } /** * Returns the RSS feed after this SAX handler has processed the XML document. */ RSSFeed feed() { return feed; } /** * Identify the appropriate dispatcher which should be used to store XML data * in a POJO. Unsupported RSS 2.0 elements are currently ignored. */ @Override public void startElement(String nsURI, String localName, String qname, org.xml.sax.Attributes attributes) { // Lookup dispatcher in hash table setter = setters.get(qname); if (setter == null) { if (RSS_ITEM.equals(qname)) { item = new RSSItem(config.categoryAvg, config.thumbnailAvg); } } else if (setter instanceof AttributeSetter) { ((AttributeSetter) setter).set(attributes); } else { // Buffer supported RSS content data buffer = new StringBuilder(); } } @Override public void endElement(String nsURI, String localName, String qname) { if (isBuffering()) { // set field of an RSS feed or RSS item ((ContentSetter) setter).set(buffer.toString()); // clear buffer buffer = null; } else if (RSS_ITEM.equals(qname)) { feed.addItem(item); // (re)enter <channel> scope item = null; } } @Override public void characters(char ch[], int start, int length) { if (isBuffering()) { buffer.append(ch, start, length); } } /** * Determines if the SAX parser is ready to receive data inside an XML element * such as <title> or <description>. * * @return boolean {@code true} if the SAX handler parses data inside an XML * element, {@code false} otherwise */ boolean isBuffering() { return buffer != null && setter != null; } }