/*
* @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 java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import org.xml.sax.Attributes;
/**
* A class to help parsing Sax Xml output. For goodreads XML output, 90% of the XML can be
* thrown away but we do need to ensure we get the tags from the right context. The XmlFilter
* objects build a tree of filters and XmlHandler objects that make this process more manageable.
*
* See SearchBooksApiHandler for an example of usage.
*
* @author Philip Warner
*/
public class XmlFilter {
/** The tag for this specific filter */
String mTagName = null;
/** A hashtable to ensure that there are no more than one sub-filter per tag at a given level */
Hashtable<String, XmlFilter> mSubFilterHash = new Hashtable<String, XmlFilter>();
/** List of sub-filters for this filter */
ArrayList<XmlFilter> mSubFilters = new ArrayList<XmlFilter>();
/** Action to perform, if any, when the associated tag is started */
XmlHandler mStartAction = null;
/** Optional parameter put in context before action is called */
Object mStartArg = null;
/** Action to perform, if any, when the associated tag is finished */
XmlHandler mEndAction = null;
/** Optional parameter put in context before action is called */
Object mEndArg = null;
/** Interface definition for filter handlers */
public interface XmlHandler {
void process(ElementContext context);
}
/** Interface definition for filter handlers */
public interface XmlHandlerExt<T> {
void process(ElementContext context, T arg);
}
/**
* Class used to define the context of a specific tag. The 'body' element will only be
* filled in the call to the 'processEnd' method.
* @author Philip Warner
*
*/
public static class ElementContext {
public String uri;
public String localName;
public String name;
public Attributes attributes;
public String preText;
public String body;
public XmlFilter filter;
public ElementContext(String uri, String localName, String name, Attributes attributes, String preText) {
this.uri = uri;
this.localName = localName;
this.name = name;
this.attributes = attributes;
this.preText = preText;
}
public Object userArg;
}
/**
* Constructor
*
* @param pattern The tag that this filter handles
*/
public XmlFilter(String pattern) {
mTagName = pattern;
}
/**
* Check if this filter matches the passed XML tag
*
* @param tag Tag name
*
* @return Boolean indicating it matches.
*/
public boolean matches(String tag) {
return mTagName.equalsIgnoreCase(tag);
}
/**
* Find a sub-filter for the passed context.
* Currently just used local_name from the context.
*
* @param context
* @return
*/
public XmlFilter getSubFilter(ElementContext context) {
return getSubFilter(context.localName);
}
/**
* Find a sub-filter based on the passed tag name.
*
* @param name XML tag name
* @return Matching filter, or NULL
*/
private XmlFilter getSubFilter(String name) {
for(XmlFilter f : mSubFilters) {
if (f.matches(name))
return f;
}
return null;
}
/**
* Called when associated tag is started.
*
* @param context
*/
public void processStart(ElementContext context) {
if (mStartAction != null) {
context.userArg = mStartArg;
mStartAction.process(context);
}
}
/**
* Called when associated tag is finished.
*
* @param context
*/
public void processEnd(ElementContext context) {
if (mEndAction != null) {
context.userArg = mEndArg;
mEndAction.process(context);
}
}
/**
* Get the tag that this filter will match
*
* @return
*/
public String getTagName() {
return mTagName;
}
/**
* Set the action to perform when the tag associated with this filter is finished.
*
* @param endAction XmlHandler to call
*
* @return This XmlFilter, to allow chaining
*/
public XmlFilter setEndAction(XmlHandler endAction) {
return setEndAction(endAction, null);
}
public XmlFilter setEndAction(XmlHandler endAction, Object userArg) {
if (mEndAction != null)
throw new RuntimeException("End Action already set");
mEndAction = endAction;
mEndArg = userArg;
return this;
}
/**
* Set the action to perform when the tag associated with this filter is started.
*
* @param startAction XmlHandler to call
*
* @return This XmlFilter, to allow chaining
*/
public XmlFilter setStartAction(XmlHandler startAction) {
return setStartAction(startAction, null);
}
public XmlFilter setStartAction(XmlHandler startAction, Object userArg) {
if (mStartAction != null)
throw new RuntimeException("Start Action already set");
mStartAction = startAction;
mStartArg = userArg;
return this;
}
/**
* Add a filter at this level; ensure it is unique.
*
* @param filter filter to add
*/
public void addFilter(XmlFilter filter) {
String lcPat = filter.getTagName().toLowerCase();
if (mSubFilterHash.containsKey(lcPat))
throw new RuntimeException("Filter " + filter.getTagName() + " already exists");
mSubFilterHash.put(lcPat, filter);
mSubFilters.add(filter);
}
/**
* Static method to add a filter to a passed tree and return the matching XmlFilter
*
* @param root Root XmlFilter object.
* @param filters Names of tags to add to tree, if not present.
*
* @return The filter matching the final tag name passed.
*/
public static XmlFilter buildFilter(XmlFilter root, String... filters ) {
if (filters.length <= 0)
return null;
return buildFilter(root, 0, Arrays.asList(filters).iterator());
}
/**
* Static method to add a filter to a passed tree and return the matching XmlFilter
*
* @param root Root XmlFilter object.
* @param filters Names of tags to add to tree, if not present.
*
* @return The filter matching the final tag name passed.
*/
public static XmlFilter buildFilter(XmlFilter root, ArrayList<String> filters ) {
if (filters.size() <= 0)
return null;
return buildFilter(root, 0, filters.iterator());
}
/**
* Internal implementation of method to add a filter to a passed tree and return the matching XmlFilter.
* This is called recursively to process the filter list.
*
* @param root Root XmlFilter object.
* @param depth Recursion depth
* @param filters Names of tags to add to tree, if not present.
*
* @return The filter matching the final tag name passed.
*/
private static XmlFilter buildFilter(XmlFilter root, int depth, Iterator<String> iter ) {
//if (!root.matches(filters[depth]))
// throw new RuntimeException("Filter at depth=" + depth + " does not match first filter parameter");
final String curr = iter.next();
XmlFilter sub = root.getSubFilter(curr);
if (sub == null) {
sub = new XmlFilter(curr);
root.addFilter(sub);
}
if (!iter.hasNext()) {
// At end
return sub;
} else {
// We are still finding leaf
return buildFilter( sub, depth+1, iter );
}
}
}