// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program 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 2 of the License, or (at your option) any later version.
//
// This program 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 this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id $
//
package com.salas.bb.utils.feedscollections;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.net.URLInputStream;
import com.salas.bb.utils.xml.XmlReaderFactory;
import com.salas.bbutilities.opml.objects.FormatConstants;
import com.salas.bbutilities.opml.utils.EmptyEntityResolver;
import com.salas.bbutilities.opml.utils.Transformation;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import javax.swing.tree.TreeNode;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Collections loading and parsing utility.
*/
class CollectionLoader
{
private static final Logger LOG = Logger.getLogger(CollectionLoader.class.getName());
/**
* Loads collection by the given URL and parses it as the list of reading lists or feeds.
*
* @param collection collection to populate with information.
* @param indexURL URL of the collection.
* @param loadReadingLists <code>TRUE</code> to return the collection of reading lists only.
* @param skipLevel <code>TRUE</code> to skip first level of folders and treat them as organization
* level fore reading list.
* @param listener listener to notify of the progress.
*/
public static void load(CollectionFolder collection, URL indexURL, boolean loadReadingLists,
boolean skipLevel, IProgressListener listener)
{
String error = null;
if (listener != null) listener.started();
try
{
Document doc = parseDocument(indexURL);
if (listener != null) listener.progress(50);
if (!isValid(doc))
{
throw new LoaderException(Strings.error("failed.to.load.the.collection"));
} else
{
// Parse
processCollection(collection, doc, loadReadingLists, skipLevel, indexURL, listener);
}
if (listener != null) listener.progress(100);
} catch (LoaderException e)
{
error = e.getMessage();
} finally
{
if (listener != null) listener.finished(error);
}
}
/**
* Loads whole collection from the document into the folder node.
*
* @param col collection folder-node.
* @param doc document.
* @param loadReadingLists <code>TRUE</code> if we currently loading reading lists.
* @param skipLevel <code>TRUE</code> to skip first level of folders and treat them as organization
* level fore reading list.
* @param baseURL base URL of the collection for correct relative links resolution.
* @param listener progress listener.
*/
private static void processCollection(CollectionFolder col, Document doc, boolean loadReadingLists,
boolean skipLevel, URL baseURL, IProgressListener listener)
{
Element root = doc.getRootElement();
Namespace bbNS = root.getNamespace(FormatConstants.BB_NS_PREFIX);
Element body = root.getChild("body");
loadToNode(col, bbNS, body, loadReadingLists, skipLevel, baseURL, listener);
}
/**
* Loads information to node.
*
* @param node node of the collections hierarchy.
* @param bbNS BlogBridge specific namespace.
* @param element element.
* @param loadReadingLists <code>TRUE</code> if we currently loading reading lists.
* @param skipLevel <code>TRUE</code> to skip first level of folders and treat them as organization
* level fore reading list.
* @param baseURL base URL of the collection for correct relative links resolution.
* @param listener progress listener.
*/
private static void loadToNode(CollectionFolder node, Namespace bbNS, Element element, boolean loadReadingLists,
boolean skipLevel, URL baseURL, IProgressListener listener)
{
List outlines = element.getChildren("outline");
for (int i = 0; i < outlines.size(); i++)
{
Element outline = (Element)outlines.get(i);
Transformation.lowercaseAttributes(outline);
String tagsS = bbNS == null ? outline.getAttributeValue("tags") : outline.getAttributeValue("tags", bbNS);
String[] tags = new String[0];
if (!StringUtils.isEmpty(tagsS)) tags = StringUtils.split(tagsS, ",");
String type = outline.getAttributeValue("type");
String title = outline.getAttributeValue("title");
String text = outline.getAttributeValue("text");
String htmlURL = outline.getAttributeValue("htmlurl");
String xmlURLS = outline.getAttributeValue("xmlurl");
if (xmlURLS == null) xmlURLS = outline.getAttributeValue("url");
URL xmlURL = null;
try
{
if (xmlURLS != null) xmlURL = new URL(baseURL, xmlURLS);
} catch (MalformedURLException e)
{
LOG.log(Level.WARNING, MessageFormat.format(
Strings.error("invalid.url"), new Object[] { xmlURL }), e);
}
if (title == null) title = text;
if (title == null) continue;
if (type == null)
{
// Folder or reading list
handleFolder(node, outline, bbNS, baseURL, title, text, tags,
htmlURL, xmlURLS, xmlURL, loadReadingLists, skipLevel, listener);
} else if ("list".equals(type))
{
// Reading list. We don't need it right now.
} else if ("link".equals(type))
{
// Possible reading list. We don't need it right now.
if (xmlURL != null && (xmlURLS.endsWith(".opml") ||
xmlURL.getPath().endsWith(".opml")))
{
handleFolder(node, outline, bbNS, baseURL, title, text, tags,
htmlURL, xmlURLS, xmlURL, loadReadingLists, skipLevel, listener);
}
} else if ("rss".equals(type) && xmlURL != null && !loadReadingLists)
{
// Feed
CollectionItem feed = new CollectionItem(title, text, tags, htmlURL, xmlURLS);
node.addNode(feed);
}
}
// Compress the view. If the only element is a folder with the same name as in this
// node, it has to be removed as a redundant level.
CollectionFolder fldr = hasDuplicateLevelFolder(node);
if (fldr != null)
{
node.nodes.clear();
Iterator en = fldr.nodes.iterator();
while (en.hasNext()) node.addNode((CollectionNode)en.next());
}
}
/**
* Returns a folder if the only child is that folder with the same name.
*
* @param node tree folder node.
*
* @return folder if the only child is that folder with the same name.
*/
private static CollectionFolder hasDuplicateLevelFolder(CollectionFolder node)
{
CollectionFolder theOnlyFolder = null;
if (node instanceof LazyCollectionFolder)
{
// Lazy collection folder needs special treatment because if
// we currently are loading it, it will go into endless recursion
// if asked for a child count or child object directly.
LazyCollectionFolder lcf = (LazyCollectionFolder)node;
if (lcf.getChildCountNoCheck() == 1 && lcf.getChildAtNoCheck(0) instanceof CollectionFolder)
{
theOnlyFolder = (CollectionFolder)lcf.getChildAtNoCheck(0);
}
} else if (node.getChildCount() == 1 && node.getChildAt(0) instanceof CollectionFolder)
{
theOnlyFolder = (CollectionFolder)node.getChildAt(0);
}
return theOnlyFolder != null &&
node.getTitle() != null &&
node.getTitle().equals(theOnlyFolder.getTitle()) ? theOnlyFolder : null;
}
/**
* Loads folder or sub-collection depending on the settings.
*
* @param node collections tree node.
* @param outline outline element.
* @param bbNS BB namespace.
* @param baseURL base URL.
* @param title title of the folder.
* @param text associated text.
* @param tags tags.
* @param htmlURL HTML URL.
* @param xmlURLS XML URL text.
* @param xmlURL XML URL.
* @param loadReadingLists <code>TRUE</code> if we currently loading reading lists.
* @param skipLevel <code>TRUE</code> to skip first level of folders and treat them as organization
* level fore reading list.
* @param listener progress listener.
*/
private static void handleFolder(CollectionFolder node, Element outline, Namespace bbNS, URL baseURL, String title,
String text, String[] tags, String htmlURL, String xmlURLS, URL xmlURL,
boolean loadReadingLists, boolean skipLevel, IProgressListener listener)
{
if (xmlURL != null)
{
// Reading list
if (loadReadingLists && (!skipLevel || !(node instanceof Collection)))
{
CollectionItem rl = new CollectionItem(title, text, tags, htmlURL, xmlURLS);
node.addNode(rl);
} else
{
// Sub-folder
node.addNode(new LazyCollectionFolder(title, text, tags, xmlURL, loadReadingLists));
}
} else
{
// Folder
CollectionFolder outlineFolder = new CollectionFolder(title, text, tags);
loadToNode(outlineFolder, bbNS, outline, loadReadingLists, false, baseURL, listener);
if (outlineFolder.getChildCount() > 0) node.addNode(outlineFolder);
}
}
/**
* Lazy folder loading its contents on demand.
*/
private static class LazyCollectionFolder extends CollectionFolder
{
private URL xmlURL;
private boolean loaded;
private boolean loadReadingLists;
/**
* Creates lazy folder.
*
* @param title title.
* @param description description.
* @param tags tags list.
* @param xmlURL XML URL of the contents.
* @param loadReadingLists <code>TRUE</code> if this folder was loaded in reading lists mode and we
*/
public LazyCollectionFolder(String title, String description, String[] tags, URL xmlURL, boolean loadReadingLists)
{
super(title, description, tags);
this.xmlURL = xmlURL;
this.loadReadingLists = loadReadingLists;
loaded = false;
}
/**
* Loads items if not loaded yet.
*/
private synchronized void loadItems()
{
if (!loaded)
{
loadSubCollection(this, xmlURL, loadReadingLists);
loaded = true;
}
}
/**
* Returns the number of children <code>TreeNode</code>s the receiver
* contains.
*/
public int getChildCount()
{
loadItems();
return super.getChildCount();
}
/**
* Returns the child <code>TreeNode</code> at index
* <code>childIndex</code>.
*/
public TreeNode getChildAt(int childIndex)
{
loadItems();
return super.getChildAt(childIndex);
}
/**
* Returns the index of <code>node</code> in the receivers children.
* If the receiver does not contain <code>node</code>, -1 will be
* returned.
*/
public int getIndex(TreeNode node)
{
loadItems();
return super.getIndex(node);
}
/**
* Returns true if the receiver is a leaf.
*/
public boolean isLeaf()
{
return loaded && super.isLeaf();
}
/**
* Returns the number of children without a loading check.
*
* @return the number of children.
*/
int getChildCountNoCheck()
{
return super.getChildCount();
}
/**
* Returns a child without a check.
*
* @param i index.
*
* @return a child.
*/
TreeNode getChildAtNoCheck(int i)
{
return super.getChildAt(i);
}
}
/**
* Loads sub-collection of items.
*
* @param node node.
* @param xmlURL XML URL of the collection.
* @param loadReadingLists <code>TRUE</code> if we currently loading reading lists.
*/
private static void loadSubCollection(CollectionFolder node, URL xmlURL, boolean loadReadingLists)
{
load(node, xmlURL, loadReadingLists, false, null);
}
/**
* Returns <code>TRUE</code> if the document is valid OPML.
*
* @param doc document to check.
*
* @return <code>TRUE</code> if the document is valid OPML.
*/
private static boolean isValid(Document doc)
{
Element root = doc.getRootElement();
if (!"opml".equalsIgnoreCase(root.getName())) return false;
Element body = root.getChild("body");
return body != null;
}
/**
* Reads OPML and parses it into the JDOM document.
*
* @param url URL to grab OPML from.
*
* @return JDOM document.
*
* @throws LoaderException if loading or parsing failed.
*/
private static Document parseDocument(URL url) throws LoaderException
{
SAXBuilder builder = new SAXBuilder(false);
// Turn off DTD loading
builder.setEntityResolver(EmptyEntityResolver.INSTANCE);
Document doc;
try
{
doc = builder.build(XmlReaderFactory.create(new URLInputStream(url)));
} catch (Exception e)
{
LOG.log(Level.SEVERE, MessageFormat.format(Strings.error("there.was.a.problem.reading.a.collection.0"), new Object[] { url }), e);
throw new LoaderException(Strings.error("there.was.a.problem.reading.a.collection"));
}
return doc;
}
/**
* Internal loader exception.
*/
private static class LoaderException extends Exception
{
/**
* Constructs a new exception with the specified detail message. The
* cause is not initialized, and may subsequently be initialized by
* a call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public LoaderException(String message)
{
super(message);
}
}
}