// 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: ImportGuidesAction.java,v 1.67 2008/03/17 16:04:50 spyromus Exp $
//
package com.salas.bb.core.actions.guide;
import com.salas.bb.core.GlobalController;
import com.salas.bb.core.GlobalModel;
import com.salas.bb.dialogs.guide.ImportGuidesDialog;
import com.salas.bb.domain.GuidesSet;
import com.salas.bb.domain.IFeed;
import com.salas.bb.domain.IGuide;
import com.salas.bb.domain.utils.GuideIcons;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.opml.BloglinesImporter;
import com.salas.bb.utils.opml.Helper;
import com.salas.bb.utils.opml.ImporterAdv;
import com.salas.bb.utils.xml.XmlReaderFactory;
import com.salas.bbutilities.opml.Importer;
import com.salas.bbutilities.opml.ImporterException;
import com.salas.bbutilities.opml.objects.OPMLGuide;
import com.salas.bbutilities.opml.objects.OPMLGuideSet;
import com.salas.bbutilities.opml.objects.OPMLReadingList;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.Set;
import java.util.logging.Logger;
/**
* Import guide from OPML resource to the list.
*
* SHOULD ALWAYS BE EXCUTED FROM EDT!
*/
public final class ImportGuidesAction extends AbstractAction
{
private static final Logger LOG = Logger.getLogger(ImportGuidesAction.class.getName());
/** @noinspection HardCodedStringLiteral*/
private static final String HTTP_REQUEST_AUTHORIZATION = "Authorization";
/** @noinspection HardCodedStringLiteral*/
private static final String BLOGLINES_SERVICE_URL = "http://rpc.bloglines.com/listsubs";
private static ImportGuidesAction instance;
/**
* Hidden constructor of singleton class.
*/
private ImportGuidesAction()
{
setEnabled(false);
}
/**
* Returns initialized instance.
*
* @return instance of action.
*/
public static synchronized ImportGuidesAction getInstance()
{
if (instance == null) instance = new ImportGuidesAction();
return instance;
}
/**
* Actual action.
*
* @param event original event object.
*/
public void actionPerformed(ActionEvent event)
{
if (GlobalController.SINGLETON.checkForNewSubscription()) return;
final ImportGuidesDialog dialog =
new ImportGuidesDialog(GlobalController.SINGLETON.getMainFrame());
dialog.open();
if (!dialog.hasBeenCanceled())
{
setEnabled(false);
try
{
processImport(dialog);
} catch (Exception e1)
{
// Just do nothing in this case. We need to be sure that this action
// will get enabled in any case.
e1.printStackTrace();
} finally
{
setEnabled(true);
}
}
}
/**
* Processes import operation basing on user entry.
*
* @param dialog dialog box.
*/
private void processImport(ImportGuidesDialog dialog)
{
final Importer importer;
final String url;
boolean fromURL = dialog.isFromURL();
final boolean isSingle = dialog.isSingleMode();
final boolean isAppending = dialog.isAppendingMode();
if (fromURL)
{
url = dialog.getUrlString();
importer = new ImporterAdv();
} else
{
String email = dialog.getBloglinesEmail();
String password = dialog.getBloglinesPassword();
url = BLOGLINES_SERVICE_URL;
importer = createBloglinesImporter(email, password);
}
// Span thread to read data separately
Thread thread = new Thread()
{
public void run()
{
doImport(importer, url, isSingle, isAppending, GlobalModel.SINGLETON, true);
}
};
thread.start();
}
/**
* Fetches data and adds it in EDT thread.
*
* @param aImporter importer to use to read data.
* @param aUrl URL to take data from.
* @param aSingle <code>TRUE</code> to fetch in single mode.
* @param aAppending <code>TRUE</code> to append.
* @param aModel data model.
* @param isConfirmationRequired <code>TRUE</code> to ask for the confirmation before importing data.
*/
public static void doImport(Importer aImporter, String aUrl, final boolean aSingle,
final boolean aAppending, final GlobalModel aModel,
final boolean isConfirmationRequired)
{
OPMLGuideSet guideSet = null;
URL baseUrl = null;
try
{
try
{
baseUrl = new URL(aUrl);
} catch (MalformedURLException e)
{
throw ImporterException.malformedUrl(e.getMessage());
}
guideSet = aImporter.process(baseUrl, aSingle);
} catch (ImporterException e)
{
processException(e);
}
if (guideSet != null)
{
final OPMLGuide[] aGuides = guideSet.getGuides();
final URL aBaseUrl = baseUrl;
// Do actual addition of data in EDT
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
if (aGuides.length > 0)
{
processImportedGuides(aModel, aBaseUrl, aGuides, aSingle || aAppending, isConfirmationRequired);
} else
{
JOptionPane.showMessageDialog(GlobalController.SINGLETON.getMainFrame(),
Strings.message("import.guides.nothing.to.import"));
}
}
});
}
}
/**
* Imports guides from a URL in append mode.
*
* @param model data model.
* @param url OPML URL.
*/
public static void importAndAppend(GlobalModel model, String url)
{
doImport(new ImporterAdv(), url, false, true, model, false);
}
/**
* Creates customized importer (handling unicode signatures) for dealing
* with foreign OPML resources. Our own OPML's do not require such processing
* and this is why this is the only place we do the following.
*
* @param username name of user to authenticate (optional).
* @param password password to user for user to authenticate (optional).
*
* @return importer object.
*/
private static Importer createBloglinesImporter(final String username, final String password)
{
return new BloglinesImporter()
{
/**
* Creates reader to use for reading data from stream.
*
* @param url URL to read data from.
*
* @return reader object.
*
* @throws java.io.IOException if opening of stream for given URL fails.
*/
public Reader createReaderForURL(URL url)
throws IOException
{
HttpURLConnection con = (HttpURLConnection)url.openConnection();
if (username != null && password != null)
{
con.setRequestProperty(HTTP_REQUEST_AUTHORIZATION,
StringUtils.createBasicAuthToken(username, password));
}
return XmlReaderFactory.create(con.getInputStream());
}
};
}
/**
* Shows confirmation dialog box with information about numbers and on confirmation
* appends or replaces the guides.
*
* @param model data model.
* @param baseURL base URL of OPML resource.
* @param guides guides.
* @param isAppending TRUE if user decided to append new guides.
* @param isConfirmationRequired TRUE to ask for confirmation before importing feeds.
*/
private static void processImportedGuides(GlobalModel model, URL baseURL, OPMLGuide[] guides,
boolean isAppending, boolean isConfirmationRequired)
{
int result = JOptionPane.YES_OPTION;
if (isConfirmationRequired)
{
int feedsCount = countFeeds(guides);
String message;
if (guides.length == 1)
{
message = MessageFormat.format(Strings.message("import.guides.ready.to.import.0.feeds"), feedsCount);
} else
{
message = MessageFormat.format(Strings.message("import.guides.ready.to.import.0.guides.with.1.feeds"),
guides.length, feedsCount);
}
message += Strings.message("import.guides.continue");
result = JOptionPane.showConfirmDialog(GlobalController.SINGLETON.getMainFrame(),
message, Strings.message("import.guides.dialog.title"), JOptionPane.YES_NO_OPTION);
}
if (result == JOptionPane.YES_OPTION)
{
final GuidesSet cgs = model.getGuidesSet();
if (isAppending)
{
appendGuides(baseURL, guides, cgs);
} else
{
replaceGuides(baseURL, guides, cgs);
}
}
}
/**
* Calculates total number of channels in guides in list.
*
* Note: package local visibility chosen to allow testing of method.
*
* @param guides list of guides.
*
* @return total number of channels.
*/
static int countFeeds(OPMLGuide[] guides)
{
int count = 0;
for (int i = 0; i < guides.length; i++)
{
OPMLGuide guide = guides[i];
count += guide.getFeeds().size();
OPMLReadingList[] lists = guide.getReadingLists();
for (int j = 0; j < lists.length; j++)
{
OPMLReadingList list = lists[j];
count += list.getFeeds().size();
}
}
return count;
}
/**
* Creates <code>ChannelGuide</code>'s, appends them to the set
* and fills with <code>ChannelGuideEntry</code>'s.
*
* Note: package local visibility chosen to allow testing of method.
*
* @param baseURL base URL of OPML resource.
* @param guides list of guides to append.
* @param set guides set to append guides to.
*/
static void appendGuides(URL baseURL, OPMLGuide[] guides, GuidesSet set)
{
// Load array list with titles already present in CGS
final Set<String> titles = set.getGuidesTitles();
for (final OPMLGuide guide : guides)
{
String title = getUniqueTitle(guide.getTitle(), titles);
appendGuide(baseURL, guide, title, set);
titles.add(title);
}
}
/**
* Appends single guide to the list.
*
* @param baseURL base URL of OPML resource.
* @param opmlGuide guide to append.
* @param uniqueTitle unique title created on basis of original title.
* @param guidesSet guide set to append guides to.
*
* @return number of feeds were actually added.
*/
static int appendGuide(URL baseURL, OPMLGuide opmlGuide, String uniqueTitle,
GuidesSet guidesSet)
{
IGuide guide = Helper.createGuide(baseURL, opmlGuide, null);
guide.setTitle(uniqueTitle);
String icon = guide.getIconKey();
if (StringUtils.isEmpty(icon))
{
icon = getUnusedIcon(guidesSet);
guide.setIconKey(icon);
}
// Replace feeds with existing -- sharing
replaceFeedsWithShares(guidesSet, guide);
// Finally add the guide
guidesSet.add(guide);
GlobalController.SINGLETON.getPoller().update(guide);
return guide.getFeedsCount();
}
/**
* Checks if the guide set contains feeds we are going to add and
* replaces them with existing.
*
* @param set set to operate.
* @param guide guide to check.
*/
static void replaceFeedsWithShares(GuidesSet set, IGuide guide)
{
IFeed[] feeds = guide.getFeeds();
for (IFeed feed : feeds)
{
IFeed existing = set.findFeed(feed);
if (existing != null && existing != feed)
{
GuidesSet.replaceFeed(feed, existing);
}
}
}
/**
* Returns first unused icon.
*
* @param set guides set to check for used icons.
*
* @return icon key.
*/
static String getUnusedIcon(GuidesSet set)
{
String icon = null;
int unusedIconIndex = GuideIcons.findUnusedIconName(set.getGuidesIconKeys());
if (unusedIconIndex != -1) icon = GuideIcons.getIconsNames()[unusedIconIndex];
return icon;
}
/**
* Checks if title is unique and if it's not then generates new one by adding
* suffix '_x' where 'x' number from 2.
*
* @param title title to check uniqueness of.
* @param titles set of already present titles.
*
* @return unique title.
*/
static String getUniqueTitle(String title, Set titles)
{
String uniqueTitle = title;
int i = 2;
while (titles.contains(uniqueTitle))
{
uniqueTitle = title + "_" + i;
i++;
}
return uniqueTitle;
}
/**
* Creates <code>ChannelGuide</code>'s, replaces with them current set
* of guides and fills with <code>ChannelGuideEntry</code>'s.
*
* Note: package local visibility chosen to allow testing of method.
*
* @param baseURL base URL of OPML resource.
* @param guides list of guides to replace with.
* @param set guides set to append guides to.
*/
public static void replaceGuides(URL baseURL, OPMLGuide[] guides, GuidesSet set)
{
set.clear();
appendGuides(baseURL, guides, set);
}
/**
* Shows appropriate warning / error dialog.
*
* @param e exception object.
*/
private static void processException(ImporterException e)
{
JOptionPane.showMessageDialog(GlobalController.SINGLETON.getMainFrame(),
ImporterException.getStringType(e),
Strings.message("import.guides.dialog.title"), JOptionPane.ERROR_MESSAGE);
}
}