/*
* @copyright 2012 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Book Catalogue is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue.goodreads.api;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.eleybourn.bookcatalogue.goodreads.api.XmlFilter.ElementContext;
/**
* Base class for parsing the output any web request that returns an XML response. NOTE: this does
* not include general web page parsing since they often do not conform to XML formatting standards.
*
* This class is used with the XmlFilter class to call user-defined code at specific points in
* an XML file.
*
* @author Philip Warner
*/
public class XmlResponseParser extends DefaultHandler {
/** Temporary storage for inter-tag text */
StringBuilder m_builder = new StringBuilder();
/** Stack of parsed tags giving context to the XML parser */
ArrayList<ElementContext> m_parents = new ArrayList<ElementContext>();
/**
* Constructor. Requires a filter tree.
*
* @param rootFilter Filter tree to use
*/
public XmlResponseParser(XmlFilter rootFilter) {
// Build the root context and add to hierarchy.
ElementContext ctx = new ElementContext(null, null, null, null, null);
ctx.filter = rootFilter;
m_parents.add(ctx);
}
/**
* Gather inter-tag text
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
super.characters(ch, start, length);
m_builder.append(ch, start, length);
}
/**
* Handle a new tag.
*/
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
super.startElement(uri, localName, name, attributes);
// Get the current context (ie. the enclosing tag)
ElementContext currElement = m_parents.get(m_parents.size()-1);
// Get the active filter for the outer context, if present
XmlFilter currFilter = currElement.filter;
// Create a new context for this new tag saving the current inter-tag text for later
ElementContext ctx = new ElementContext(uri, localName, name, attributes, m_builder.toString());
// If there is an active filter, then see if the new tag is of any interest
if (currFilter != null) {
// Check for interest in new tag
XmlFilter filter = currElement.filter.getSubFilter(ctx);
// If new tag has a filter, store it in the new context object
ctx.filter = filter;
// If we got a filter, tell it a tag is now starting.
if (filter != null)
filter.processStart(ctx);
}
// Add the new tag to the context hierarchy and reset
m_parents.add(ctx);
// Reset the inter-tag text storage.
m_builder.setLength(0);
}
/**
* Handle the end of the current tag
*/
@Override
public void endElement(String uri, String localName, String name) throws SAXException {
super.endElement(uri, localName, name);
// Get out current context from the hierarchy and pop from stack
ElementContext thisElement = m_parents.remove(m_parents.size()-1);
// Minor paranoia. Make sure name matches. Total waste of time, right?
if (!thisElement.localName.equals(localName)) {
throw new RuntimeException("End element '" + localName + "' does not match start element '" + thisElement.localName + "'");
}
// Save the text that appeared inside this tag (but not inside inner tags)
thisElement.body = m_builder.toString();
// If there is an active filter in this context, then tell it the tag is finished.
if (thisElement.filter != null) {
thisElement.filter.processEnd(thisElement);
}
// Reset the inter-tag text and and append the previously saved 'pre-text'.
m_builder.setLength(0);
m_builder.append(thisElement.preText);
}
}