// 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: GlobalController.java,v 1.389 2008/04/09 04:34:38 spyromus Exp $
//
package com.salas.bb.core;
import com.jgoodies.binding.value.ValueHolder;
import com.jgoodies.uif.application.Application;
import com.jgoodies.uif.application.ApplicationAdapter;
import com.jgoodies.uif.application.ApplicationEvent;
import com.jgoodies.uif.util.ResourceUtils;
import com.jgoodies.uif.util.SystemUtils;
import com.salas.bb.core.actions.feed.FeedLinkPostToBlogAction;
import com.salas.bb.core.actions.guide.SubscribeToReadingListAction;
import com.salas.bb.core.autosave.AutoSaver;
import com.salas.bb.dialogs.*;
import com.salas.bb.discovery.*;
import com.salas.bb.discovery.filter.CompositeURLFilter;
import com.salas.bb.discovery.filter.DynamicExtensionURLFilter;
import com.salas.bb.discovery.filter.ExtensionURLFilter;
import com.salas.bb.domain.*;
import com.salas.bb.domain.prefs.StarzPreferences;
import com.salas.bb.domain.prefs.UserPreferences;
import com.salas.bb.domain.query.ICriteria;
import com.salas.bb.domain.query.articles.ArticleTextProperty;
import com.salas.bb.domain.query.articles.Query;
import com.salas.bb.domain.query.general.StringContainsCO;
import com.salas.bb.domain.querytypes.QueryType;
import com.salas.bb.domain.utils.DomainEventsListener;
import com.salas.bb.domain.utils.GuidesUtils;
import com.salas.bb.domain.utils.IDomainListener;
import com.salas.bb.imageblocker.ImageBlocker;
import com.salas.bb.networking.manager.NetManager;
import com.salas.bb.persistence.ChangesMonitor;
import com.salas.bb.persistence.IPersistenceManager;
import com.salas.bb.persistence.PersistenceManagerConfig;
import com.salas.bb.plugins.Manager;
import com.salas.bb.plugins.domain.AdvancedPreferencesPlugin;
import com.salas.bb.plugins.domain.IPlugin;
import com.salas.bb.plugins.domain.Package;
import com.salas.bb.remixfeeds.PostToBlogAction;
import com.salas.bb.search.SearchEngine;
import com.salas.bb.sentiments.ArticleFilterProtector;
import com.salas.bb.sentiments.DomainListener;
import com.salas.bb.sentiments.SentimentsConfig;
import com.salas.bb.service.ServerService;
import com.salas.bb.service.ServicePreferences;
import com.salas.bb.service.sync.SyncFull;
import com.salas.bb.service.sync.SyncFullAction;
import com.salas.bb.service.sync.SyncOut;
import com.salas.bb.tags.TagsRepository;
import com.salas.bb.tags.TagsSaver;
import com.salas.bb.tags.net.*;
import com.salas.bb.updates.FullCheckCycle;
import com.salas.bb.utils.*;
import com.salas.bb.utils.discovery.DiscoveryResult;
import com.salas.bb.utils.discovery.UrlDiscovererException;
import com.salas.bb.utils.discovery.detector.XMLFormat;
import com.salas.bb.utils.discovery.detector.XMLFormatDetector;
import com.salas.bb.utils.discovery.impl.DirectDiscoverer;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.ipc.IIPCListener;
import com.salas.bb.utils.notification.NotificationArea;
import com.salas.bb.utils.poller.Poller;
import com.salas.bb.utils.uif.UifUtilities;
import com.salas.bb.utils.uif.images.ImageFetcher;
import com.salas.bb.views.*;
import com.salas.bb.views.feeds.IFeedDisplay;
import com.salas.bb.views.mainframe.MainFrame;
import com.salas.bb.views.mainframe.UnreadButton;
import com.salas.bb.views.stylesheets.StylesheetManager;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Implements all the behaviors of commands in the app. A key design question is whether the
* selection is part of the model or of the controller. I concluded that to have the stickyness of
* selections in channels, and items, it should be part of the model.
*/
public final class GlobalController implements IIPCListener
{
private static final Logger LOG = Logger.getLogger(GlobalController.class.getName());
private static final String AUTO_GUIDE_TITLE = Strings.message("automatically.created.guide.title");
/** Key for preference to store selected guide ID between application runs. */
private static final String KEY_SELECTED_GUIDE_ID = "selectedGuideId";
/** Key for preference to store selected feed ID between application runs. */
private static final String KEY_SELECTED_FEED_ID = "selectedFeedId";
/** Number of last backups to keep in backups directory. */
private static final int LAST_BACKUPS_TO_KEEP = 10;
private static final String THREAD_NAME_SEARCH_QUERY = "Run Search Feed Query";
/**
* Minimum number of feeds to have saved during previous sync-out to start looking for
* suspicious decreases.
*/
static final int CHANGE_CHECK_FEEDS_THRESHOLD = 50;
/**
* Number of times the current feeds count should be less than the previous to
* sound the alarm.
*/
static final int CHANGE_CHECK_DIFF_TIMES = 3;
/**
* Singleton instance.
*/
public static final GlobalController SINGLETON = new GlobalController();
private List<IControllerListener> listeners = new CopyOnWriteArrayList<IControllerListener>();
private boolean initializationFinished = false;
private GlobalModel model;
private MDManager metaDataManager;
private MDUpdater metaDataUpdater;
// Navigation component.
private NavigatorAdv navigator;
private NavigatorAdapter navigatorAdapter;
private ScoresCalculator scoresCalculator;
private HighlightsCalculator highlightsCalculator;
private PropertyChangeDispatcher propertyChangeDispatcher;
private BackgroundProccessManager backManager;
private GuideModel navigationModel;
private MainFrame mainFrame;
private SelectedFeedListener selectedFeedListener;
private DeletedObjectsRepository deletedObjectsRepository;
private SearchFeedsManager searchFeedsManager;
private DomainEventsListener domainEventsListener;
private HighlightsCalculator searchHighlightsCalculator;
private String currentSearchKeywords;
/** Tags manager controls the viewing and editing the tags of taggable objects. */
private TagsSaver tagsSaver;
private WrappingStorage tagsStorage;
private Poller poller;
private URL hoveredLink;
private SearchEngine searchEngine;
private EventsNotifier eventNotifier;
DockIconUnreadMonitor dockIconUnreadMonitor;
private FeatureManager featureManager;
private GuidesListModel guidesListModel;
private PinTagger pinTagger;
private AutoSaver autoSaver;
/** A link that should be highlighted in an article when found. */
private String highlightedArticleLink;
/**
* Constructor of the GlobalController. Note that this is called early in the launch of the
* application, before any of the UI has been built. Do not call any UI related methods here
* because they will fail.
*/
private GlobalController()
{
if (LOG.isLoggable(Level.FINE)) LOG.fine("Constructing GlobalController");
hoveredLink = null;
AbstractFeed.setFeedVisibilityResolver(new IFeedVisibilityResolver()
{
public boolean isVisible(IFeed feed)
{
return feed != null &&
FeedDisplayModeManager.getInstance().isVisible(feed.getClassesMask());
}
});
featureManager = new FeatureManager(Application.getUserPreferences());
if (NotificationArea.isSupported())
{
eventNotifier = new EventsNotifier();
eventNotifier.setSoundResourceID("sound.new.articles");
}
searchEngine = new SearchEngine();
backManager = new BackgroundProccessManager();
pinTagger = new PinTagger(this);
autoSaver = new AutoSaver();
if (SystemUtils.IS_OS_MAC) dockIconUnreadMonitor = new DockIconUnreadMonitor();
selectedFeedListener = new SelectedFeedListener();
deletedObjectsRepository = new DeletedObjectsRepository(PersistenceManagerConfig.getManager());
searchHighlightsCalculator = new HighlightsCalculator();
currentSearchKeywords = "";
highlightsCalculator = new HighlightsCalculator();
scoresCalculator = new ScoresCalculator();
guidesListModel = new GuidesListModel();
navigationModel = new GuideModel(scoresCalculator, false,
FeedDisplayModeManager.getInstance());
navigator = new NavigatorAdv(navigationModel, guidesListModel);
addControllerListener(navigator);
navigatorAdapter = new NavigatorAdapter();
propertyChangeDispatcher = new PropertyChangeDispatcher(this);
setupTagsSupport();
setupMetaDataSupport();
setupPolling();
// register markers
addControllerListener(ArticleMarker.getInstance());
setModel(new GlobalModel(scoresCalculator));
GlobalModel.setSINGLETON(model);
// Add sentiments domain listener
addDomainListener(new DomainListener());
}
/**
* Returns the guides list model.
*
* @return model.
*/
public GuidesListModel getGuidesListModel()
{
return guidesListModel;
}
/**
* Returns current feature manager.
*
* @return manager.
*/
public FeatureManager getFeatureManager()
{
return featureManager;
}
/** Configures and schedules stylesheets updater. */
private void setupStylesUpdater()
{
backManager.schedule(StylesheetManager.getUpdater(), 20, 3600);
}
/**
* Returns current deleted feeds repository instance.
*
* @return instance.
*/
public DeletedObjectsRepository getDeletedFeedsRepository()
{
return deletedObjectsRepository;
}
/**
* Reselect currently selected feed in the midnight to let it regroup the articles.
*/
private void setupFeedReselector()
{
long delayToTomorrow = DateUtils.getTomorrowTime() - System.currentTimeMillis();
// Add a second to be sure that tomorrow has come
delayToTomorrow += 1000L;
// Call this code every midnight
backManager.schedule(new Runnable()
{
public void run()
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
IFeed selFeed = getModel().getSelectedFeed();
if (selFeed != null)
{
selectFeed(null);
selectFeed(selFeed, true);
}
}
});
}
}, delayToTomorrow / Constants.MILLIS_IN_SECOND, Constants.SECONDS_IN_DAY);
}
/** Configures polling. */
private void setupPolling()
{
ConnectionState connectionState = getConnectionState();
poller = new Poller(connectionState);
}
/**
* Returns current connection state object.
*
* @return connection state object.
*/
public static ConnectionState getConnectionState()
{
return ApplicationLauncher.getConnectionState();
}
/** Configures meta-data support. */
private void setupMetaDataSupport()
{
ConnectionState connectionState = getConnectionState();
metaDataManager = new MDManager(connectionState);
metaDataManager.addDiscoveryListener(new DiscoveryListener());
metaDataUpdater = new MDUpdater(metaDataManager, connectionState);
}
/** Configures and installs tags support. */
private void setupTagsSupport()
{
tagsStorage = new WrappingStorage(new EmptyStorage());
tagsSaver = new TagsSaver(tagsStorage);
}
/**
* Changes storage to a new type.
*
* @param aNewType new type.
*/
public void changeTagsStorage(int aNewType)
{
ITagsStorage storage;
switch (aNewType)
{
case UserPreferences.TAGS_STORAGE_DELICIOUS:
storage = new DeliciousStorage(new UserPreferencesCallback());
break;
case UserPreferences.TAGS_STORAGE_BB_SERVICE:
storage = new BBServiceStorage(new BBServiceCredentialCallback());
break;
default:
storage = new EmptyStorage();
break;
}
tagsStorage.setCurrentStorage(storage);
}
/**
* Returns currently selected tags networker component.
*
* @return networker component.
*/
public ITagsStorage getTagsStorage()
{
return tagsStorage;
}
/**
* Sets new model to control.
*
* @param aModel model.
*/
private void setModel(GlobalModel aModel)
{
if (model != null) uninstallModel();
model = aModel;
if (model != null) installModel();
}
/**
* Registers application main frame.
*
* @param aMainFrame main frame object.
*/
public void setMainFrame(MainFrame aMainFrame)
{
if (mainFrame == aMainFrame) return;
mainFrame = aMainFrame;
if (eventNotifier != null) eventNotifier.setFrame(aMainFrame);
mainFrame.setMinimizeToSystemTray(model.getUserPreferences().isMinimizeToSystray());
// Search functionality setup
// mainFrame.getSearchField().setAppIconActionListener(new SearchFieldListener());
// mainFrame.setSearchResult(searchEngine.getResult());
}
/** Installs new model. */
private void installModel()
{
final GuidesSet guidesSet = model.getGuidesSet();
final StarzPreferences starzPreferences = model.getStarzPreferences();
final UserPreferences userPreferences = model.getUserPreferences();
userPreferences.addPropertyChangeListener(propertyChangeDispatcher);
starzPreferences.addPropertyChangeListener(propertyChangeDispatcher);
scoresCalculator.loadPreferences(starzPreferences);
ArticleFilterProtector.init();
navigator.setViewModel(model.getGuideModel());
navigator.guideSelected(model.getSelectedGuide());
navigator.feedSelected(model.getSelectedFeed());
navigator.setGuidesSet(guidesSet);
domainEventsListener = new DomainEventsListener(guidesSet);
searchFeedsManager = new SearchFeedsManager(guidesSet);
domainEventsListener.addDomainListener(searchFeedsManager);
if (eventNotifier != null) domainEventsListener.addDomainListener(eventNotifier);
domainEventsListener.addDomainListener(deletedObjectsRepository);
if (dockIconUnreadMonitor != null)
{
dockIconUnreadMonitor.setSet(guidesSet);
addControllerListener(dockIconUnreadMonitor.getMonitor());
domainEventsListener.addDomainListener(dockIconUnreadMonitor);
userPreferences.addPropertyChangeListener(dockIconUnreadMonitor);
FeedDisplayModeManager.getInstance().addListener(dockIconUnreadMonitor);
}
guidesListModel.setGuidesSet(guidesSet);
addDomainListener(guidesListModel.getDomainListener());
userPreferences.addPropertyChangeListener(guidesListModel.getUserPreferencesListener());
addControllerListener(guidesListModel.getControllerListener());
pinTagger.setUserPreferences(userPreferences);
// This listener should go after the searchFeedsManager
addDomainListener(autoSaver);
tagsSaver.setGuidesSet(guidesSet);
changeTagsStorage(userPreferences.getTagsStorage());
metaDataUpdater.setGuidesSet(guidesSet);
poller.setGuidesSet(guidesSet);
poller.update();
searchEngine.setGuidesSet(guidesSet);
if (eventNotifier != null) eventNotifier.setUserPreferences(userPreferences);
// Setup URL filter
CompositeURLFilter urlFilter = new CompositeURLFilter();
urlFilter.addFilter(new ExtensionURLFilter(ResourceUtils.getString(ResourceID.NO_DISCOVERY_EXTENSIONS)));
urlFilter.addFilter(new DynamicExtensionURLFilter(model.getUserPreferences(),
UserPreferences.PROP_NO_DISCOVERY_EXTENSIONS));
MDDiscoveryLogic.setURLFilter(urlFilter);
featureManager.setServicePreferences(model.getServicePreferences());
}
/** Uninstalls the model. */
private void uninstallModel()
{
final StarzPreferences starzPreferences = model.getStarzPreferences();
final UserPreferences userPreferences = model.getUserPreferences();
starzPreferences.removePropertyChangeListener(propertyChangeDispatcher);
userPreferences.removePropertyChangeListener(propertyChangeDispatcher);
navigator.setViewModel(null);
navigator.setGuidesSet(null);
navigator.guideSelected(null);
navigator.feedSelected(null);
tagsSaver.setGuidesSet(null);
searchEngine.setGuidesSet(null);
guidesListModel.setGuidesSet(null);
// Reset URL filter
MDDiscoveryLogic.setURLFilter(null);
}
/**
* Returns current model.
*
* @return model.
*/
public GlobalModel getModel()
{
return model;
}
/**
* Returns the search engine.
*
* @return engine.
*/
public SearchEngine getSearchEngine()
{
return searchEngine;
}
/**
* Returns navigation listener.
*
* @return navigation listener.
*/
public IArticleListNavigationListener getNavigationListener()
{
return navigatorAdapter;
}
/**
* Returns navigator guide model.
*
* @return navigator model.
*/
GuideModel getNavigationModel()
{
return navigationModel;
}
/**
* Returns the background process manager.
*
* @return background manager.
*/
public BackgroundProccessManager getBackgroundProccessManager()
{
return backManager;
}
/**
* Returns a search feeds manager.
*
* @return manager.
*/
public static SearchFeedsManager getSearchFeedsManager()
{
return SINGLETON.searchFeedsManager;
}
/**
* Changes current guide selection.
*
* @param guide new guide to select.
*/
public void selectGuideAndFeed(final IGuide guide)
{
selectGuide(guide, true);
}
/**
* Changes current guide selection.
*
* @param guide new guide to select.
* @param selectFeed TRUE to select currently selected feed.
*/
public void selectGuide(final IGuide guide, final boolean selectFeed)
{
final boolean alreadySelected = model.getSelectedGuide() == guide;
if (!isInitializationFinished() || !alreadySelected)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("selectGuide: " + (guide == null ? "null" : guide.getTitle()));
}
SelectGuideTask task = new SelectGuideTask(guide, selectFeed, alreadySelected);
if (UifUtilities.isEDT()) task.run(); else SwingUtilities.invokeLater(task);
}
}
/**
* Selects the feed.
*
* @param feed feed to select.
*/
public void selectFeed(IFeed feed)
{
// EDT !!!!
selectFeed(feed, false);
}
/**
* Selects the feed.
*
* @param feed feed to select.
* @param selectHidden <code>TRUE</code> to allow hidden feeds selection.
*/
public void selectFeed(IFeed feed, boolean selectHidden)
{
// EDT !!!!
if (LOG.isLoggable(Level.FINE)) LOG.fine("selectFeed: " + feed);
highlightedArticleLink = null;
// Unregister our listener from selected feed
IFeed selectedFeed = model.getSelectedFeed();
if (selectedFeed != null) selectedFeed.removeListener(selectedFeedListener);
GuideModel guideModel = model.getGuideModel();
// Allow selection only if
// (a) feed belongs to current guide, and
// (b) select hidden feeds mode is on or feed is visible
if (!model.isSelectable(feed) ||
(!selectHidden && guideModel.indexOf(feed) == -1)) feed = null;
// Register the listener to get events about articles manipulations and other feed changes
if (feed != null) feed.addListener(selectedFeedListener);
// We have to have this to ensure that even if we have "select hidden" mode disabled
// we can have currently selected feed preserved from disappearing on starz filter changes.
navigationModel.ensureVisibilityOf(feed);
guideModel.ensureVisibilityOf(feed);
// We need to select feed first order to release selection from previously selected feed
// *before* calling the model change. Otherwise it will reflect in another loop and
// incorrect selection.
if (mainFrame != null) mainFrame.selectFeed(feed);
final IFeed oldFeed = model.getSelectedFeed();
ViewModeValueModel vmvm = model.getViewModeValueModel();
ViewTypeValueModel vtvm = model.getViewTypeValueModel();
vmvm.recordValue();
vtvm.recordValue();
model.setSelectedFeed(feed);
updateSearchHighlights(oldFeed, feed);
// Abort loading of all images in previously selected feeds
ImageFetcher.clearQueue();
fireFeedSelected(feed);
vmvm.compareRecordedWithCurrent();
vtvm.compareRecordedWithCurrent();
// Reset selected article when no feed selected
if (feed == null) selectArticle(null);
}
/**
* Selects article.
*
* @param aArticle article.
*/
public void selectArticle(final IArticle aArticle)
{
selectArticle(aArticle, null);
}
/**
* Selects article.
*
* @param aArticle article.
* @param highlightLink a link to highlight.
*/
public void selectArticle(final IArticle aArticle, final String highlightLink)
{
if (aArticle != null)
{
IFeed feed = aArticle.getFeed();
if (model.getSelectedFeed() != feed)
{
// Selecting feed first
IGuide[] guides = feed.getParentGuides();
boolean guideSelected = false;
IGuide currentGuide = model.getSelectedGuide();
for (int i = 0; !guideSelected && i < guides.length; i++)
{
IGuide guide = guides[i];
guideSelected = (guide == currentGuide);
}
if (!guideSelected) selectGuide(chooseBestGuide(guides), false);
selectFeed(feed, true);
}
}
this.highlightedArticleLink = highlightLink;
// Selecting article
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
model.setSelectedArticles(new IArticle[] { aArticle });
model.setSelectedArticle(aArticle);
fireArticleSelected(aArticle);
}
});
}
/**
* Returns a link that should be highlighted in an article when found.
*
* @return a link or <code>NULL</code>.
*/
public String getHighlightedArticleLink()
{
return highlightedArticleLink;
}
/**
* Sets or resets the search highlights depending on the type of the selected feed.
*
* @param oldFeed previously selected feed.
* @param newFeed newly selected feed.
*
* @return <code>TRUE</code> if repainting required.
*/
private boolean updateSearchHighlights(IFeed oldFeed, IFeed newFeed)
{
boolean repaintHighlights = false;
if (newFeed instanceof SearchFeed)
{
repaintHighlights = installHighlightsFromSearchFeed((SearchFeed)newFeed);
} else if (oldFeed != null && oldFeed instanceof SearchFeed)
{
resetSearchHighlights();
repaintHighlights = true;
}
return repaintHighlights;
}
private void resetSearchHighlights()
{
searchHighlightsCalculator.keywordsChanged(Constants.EMPTY_STRING);
currentSearchKeywords = Constants.EMPTY_STRING;
}
private boolean installHighlightsFromSearchFeed(SearchFeed aFeed)
{
boolean installed = false;
String newKeywords = collectKeywordsFromSearchFeed(aFeed);
if (!newKeywords.equalsIgnoreCase(currentSearchKeywords))
{
searchHighlightsCalculator.keywordsChanged(newKeywords);
currentSearchKeywords = newKeywords;
installed = true;
}
return installed;
}
private String collectKeywordsFromSearchFeed(SearchFeed aFeed)
{
StringBuffer keywords = new StringBuffer();
Query query = aFeed.getQuery();
int criteriaCount = query.getCriteriaCount();
for (int i = 0; i < criteriaCount; i++)
{
ICriteria criteria = query.getCriteriaAt(i);
if (isKeywordsSearchCriteria(criteria))
{
String keywordsList = criteria.getValue();
String[] keywordsArray = StringUtils.keywordsToArray(keywordsList);
// Quote if necessary
for (int j = 0; j < keywordsArray.length; j++)
{
keywordsArray[j] = StringUtils.quoteKeywordIfNecessary(keywordsArray[j]);
}
keywords.append("\n").append(StringUtils.join(keywordsArray, "\n"));
}
}
return keywords.toString().trim();
}
private static boolean isKeywordsSearchCriteria(ICriteria aCriteria)
{
return ArticleTextProperty.INSTANCE.equals(aCriteria.getProperty()) &&
StringContainsCO.INSTANCE.equals(aCriteria.getComparisonOperation());
}
/**
* Returns main application frame.
*
* @return main frame.
*/
public MainFrame getMainFrame()
{
return mainFrame;
}
/**
* Moves all channels to the other guide.
*
* @param srcGuide source guide.
* @param destGuide destination guide.
*/
public void reassignChannelsTo(final StandardGuide srcGuide, final StandardGuide destGuide)
{
if (destGuide == null || destGuide == srcGuide) return;
IFeed[] feeds = null;
synchronized (srcGuide)
{
int srcFeedsCount = srcGuide.getFeedsCount();
if (srcFeedsCount > 0)
{
feeds = new IFeed[srcFeedsCount];
for (int i = srcFeedsCount - 1; i >= 0; i--)
{
feeds[i] = srcGuide.getFeedAt(i);
srcGuide.remove(feeds[i]);
}
}
}
if (feeds != null)
{
synchronized (destGuide)
{
for (IFeed feed : feeds) destGuide.add(feed);
}
}
}
/**
* Merges the list of guides with other guide.
*
* @param aGuides guides to merge with the target guide (will be removed).
* @param aMergeGuide target guide to merge with.
*/
public void mergeGuides(final IGuide[] aGuides, final StandardGuide aMergeGuide)
{
for (IGuide aGuide : aGuides)
{
if (aGuide instanceof StandardGuide)
{
reassignChannelsTo((StandardGuide)aGuide, aMergeGuide);
getModel().getGuidesSet().remove(aGuide);
}
}
selectGuideAndFeed(aMergeGuide);
}
// Starts background processes
private void startBackgroundProcesses()
{
// The sequence is imporatant!
// We wish this code to be executed after the model is set
backManager.schedule(featureManager.getUpdater(), FeatureManager.UPDATE_PERIOD_SEC);
backManager.schedule(tagsSaver, 60, 10);
if (System.getProperty("noMetaDataUpdates") == null) backManager.schedule(metaDataUpdater, 60, 60);
backManager.schedule(poller, 30, 10);
// Start post-initialization tasks
setupFeedReselector();
setupStylesUpdater();
// Start SearchFeeds updates
backManager.scheduleOnce(new Runnable()
{
public void run()
{
searchFeedsManager.runAllQueries();
}
}, 20);
}
/**
* Returns meta-data manager.
*
* @return meta-data manager.
*/
public MDManager getMetaDataManager()
{
return metaDataManager;
}
/**
* Returns current highlights calculator.
*
* @return highlights calculator.
*/
public HighlightsCalculator getHighlightsCalculator()
{
return highlightsCalculator;
}
/**
* Returns current search highlights calculator.
*
* @return search highlights calculator.
*/
public HighlightsCalculator getSearchHighlightsCalculator()
{
return searchHighlightsCalculator;
}
/**
* Returns channel score calculator.
*
* @return channel score calculator.
*/
public ScoresCalculator getScoreCalculator()
{
return scoresCalculator;
}
/**
* Invoked by InformaBackEnd when it finds out that there's no resource under feed's URL.
*
* @param feed feed which was polled.
*/
public void feedHasGone(DirectFeed feed)
{
if (!feed.isDynamic())
{
FeedGoneDialog dialog = new FeedGoneDialog(getMainFrame(), feed);
dialog.open();
if (!dialog.hasBeenCanceled())
{
IGuide[] guides = feed.getParentGuides();
for (IGuide guide : guides) guide.remove(feed);
}
}
}
/**
* Invoked when it's detected that feed has moved to a new location.
*
* @param feed moved feed.
* @param newLocation new location.
*/
public void feedHasMoved(DirectFeed feed, URL newLocation)
{
if (feed == null)
throw new IllegalArgumentException(Strings.error("unspecified.feed"));
if (newLocation == null)
throw new IllegalArgumentException(Strings.error("unspecified.location"));
DirectFeed existingFeed = getModel().getGuidesSet().findDirectFeed(newLocation);
if (existingFeed != null)
{
GuidesSet.replaceFeed(feed, existingFeed);
} else
{
feed.setXmlURL(newLocation);
}
}
/**
* Indicates that guide has changed its position in list.
*
* @param guide guide.
* @param newPosition new position.
*/
public void guideMoved(IGuide guide, int newPosition)
{
GuidesPanel cgp = mainFrame.getGudiesPanel();
cgp.ensureIndexIsVisible(newPosition);
}
/**
* Immediately starts updating of the feed.
*
* @param aFeed feed to update.
*/
public void updateFeed(IFeed aFeed)
{
if (aFeed instanceof DataFeed)
{
DataFeed dFeed = (DataFeed)aFeed;
poller.update(dFeed, true, true);
} else if (aFeed instanceof SearchFeed)
{
updateSearchFeed((SearchFeed)aFeed);
}
}
/**
* Orders meta-data manager to forget given meta-data holders and refreshes
* articles' highlights.
*
* @param holders holders to forget.
*/
public void forgetDiscoveries(FeedMetaDataHolder[] holders)
{
metaDataManager.forget(holders);
repaintArticlesListHighlights();
}
/**
* Returns currently active poller.
*
* @return poller.
*/
public Poller getPoller()
{
return poller;
}
/**
* Deletes guides and selects appropriate guide after that.
*
* @param guides guides to delete.
*/
public void deleteGuides(IGuide[] guides)
{
IGuide currentGuide = model.getSelectedGuide();
GuidesSet set = model.getGuidesSet();
int removedIndex = -1;
for (IGuide guide : guides)
{
if (guide == currentGuide)
{
removedIndex = set.indexOf(guide);
}
set.remove(guide);
}
if (removedIndex != -1) selectGuideAndFeed(findGuideToSelect(set, removedIndex));
}
/**
* Returns the guide to be selected after removal of the other guide from guides set.
*
* @param guidesSet the set.
* @param removedIndex index of the removed guide.
*
* @return guide to select or <code>NULL</code> if it was the last guide.
*/
static IGuide findGuideToSelect(GuidesSet guidesSet, int removedIndex)
{
IGuide guideToSelect = null;
if (removedIndex >= 0)
{
GuideDisplayModeManager gdmm = GuideDisplayModeManager.getInstance();
int count = guidesSet.getGuidesCount();
for (int i = removedIndex; guideToSelect == null && i < count; i++)
{
IGuide g = guidesSet.getGuideAt(i);
if (gdmm.isVisible(g)) guideToSelect = g;
}
for (int i = removedIndex - 1; guideToSelect == null && i >= 0; i--)
{
IGuide g = guidesSet.getGuideAt(i);
if (gdmm.isVisible(g)) guideToSelect = g;
}
}
return guideToSelect;
}
/**
* Updates given reading list.
*
* @param list list to update.
* @param addFeeds feeds to add to the list.
* @param removeFeeds feeds to remove from the guide.
*/
public static void updateReadingList(ReadingList list, List<DirectFeed> addFeeds,
List<DirectFeed> removeFeeds)
{
if (list == null) throw new NullPointerException(Strings.error("unspecified.reading.list"));
if (addFeeds == null) throw new NullPointerException(Strings.error("unspecified.feeds.to.add"));
if (removeFeeds == null) throw new NullPointerException(Strings.error("unspecified.feeds.to.remove"));
if (addFeeds.size() == 0 && removeFeeds.size() == 0) return;
int action = SINGLETON.getModel().getUserPreferences().getOnReadingListUpdateActions();
// Update only non-removed lists when expecting no confirmation or when confirmation is granted
boolean doUpdates = list.getParentGuide() != null &&
(action != UserPreferences.RL_UPDATE_CONFIRM ||
confirmReadingListUpdates(list, addFeeds, removeFeeds));
if (doUpdates)
{
GuidesSet set = SINGLETON.getModel().getGuidesSet();
for (DirectFeed feed : addFeeds)
{
URL xmlURL = feed.getXmlURL();
DirectFeed existingFeed = set.findDirectFeed(xmlURL);
if (existingFeed != null)
{
feed = existingFeed;
} else
{
SINGLETON.getPoller().update(feed, false);
}
list.add(feed);
}
for (DirectFeed removeFeed : removeFeeds) list.remove(removeFeed);
if (action == UserPreferences.RL_UPDATE_NOTIFY)
{
showReadingListUpdateNotification(list, addFeeds, removeFeeds);
}
}
}
/**
* Simple notification dialog with a summary of updates.
*
* @param list list updated.
* @param addFeeds feeds added.
* @param removeFeeds feeds removed.
*/
private static void showReadingListUpdateNotification(ReadingList list, List addFeeds,
List removeFeeds)
{
String msg = MessageFormat.format(Strings.message("readinglist.updates.message"),
list.getTitle(), list.getParentGuide().getTitle(),
addFeeds.size(), removeFeeds.size());
JOptionPane.showMessageDialog(SINGLETON.getMainFrame(), msg, Strings.message("readinglist.updates.title"),
JOptionPane.INFORMATION_MESSAGE);
}
/**
* Shows the dialog box with the list of URL's which are going to be added and the
* list of feeds which are going to be removed and asks for confirmation from a user.
*
* @param list reading list.
* @param addFeeds list of feeds to add.
* @param removeFeeds list of feeds to remove.
*
* @return <code>TRUE</code> if modification has been accepted.
*/
private static boolean confirmReadingListUpdates(ReadingList list, List<DirectFeed> addFeeds,
List<DirectFeed> removeFeeds)
{
ReadingListUpdateConfirmationDialog dialog =
new ReadingListUpdateConfirmationDialog(SINGLETON.getMainFrame(),
list, addFeeds, removeFeeds);
dialog.open();
boolean confirmed = false;
if (!dialog.hasBeenCanceled())
{
List newAddFeeds = dialog.getAddFeeds();
addFeeds.clear();
addFeeds.addAll(newAddFeeds);
List newRemoveFeeds = dialog.getRemoveFeeds();
removeFeeds.clear();
removeFeeds.addAll(newRemoveFeeds);
confirmed = true;
}
return confirmed;
}
/**
* Returns the best guide of all to use for feed selection.
* When the feed is in the same guide we are at this guide is preferred.
*
* @param guides guides.
*
* @return the best guide to use.
*/
public static IGuide chooseBestGuide(IGuide[] guides)
{
IGuide guide = null;
IGuide currentGuide = GlobalModel.SINGLETON.getSelectedGuide();
if (currentGuide != null)
{
// Select current guide
for (int i = 0; guide == null && i < guides.length; i++)
{
IGuide iguide = guides[i];
if (currentGuide == iguide) guide = currentGuide;
}
}
// If the guide is still unselected, select the first guide
if (guide == null) guide = guides[0];
return guide;
}
private void autosubscribeIfNecessary()
{
String urlToOpen = ApplicationLauncher.getURLToOpen();
if (StringUtils.isNotEmpty(urlToOpen))
{
try
{
subscribe(new URL(urlToOpen));
} catch (MalformedURLException e)
{
LOG.warning("Invalid URL specified for subscription.");
}
}
}
/**
* Subscribe to a given URL.
*
* @param url URL to subscribe to.
*/
public void subscribe(final URL url)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
subscribe(url, XMLFormatDetector.detectOrAskFormat(url, getMainFrame()));
}
});
}
/**
* Subscribes to a given URL with known format.
*
* @param url URL to subscribe to.
* @param fmt format.
*/
public void subscribe(URL url, XMLFormat fmt)
{
if (fmt != null)
{
if (checkForNewSubscription()) return;
if (fmt != XMLFormat.OPML)
{
// TODO: Ask what to do ???
DirectFeed feed = createDirectFeed(null, url);
if (feed != null) selectFeed(feed, true);
} else
{
SubscribeToReadingListAction.subscribe(url);
}
} else
{
// TODO: Localize
JOptionPane.showMessageDialog(getMainFrame(),
"<html><b>Unrecognized format of file.</b>\n\n" +
"BlogBridge can't recognize the format of the file.\n" +
"Please verify that the file is XML feed (RSS, Atom) or\n" +
"Reading List (OPML) and try again.",
"Subscribe",
JOptionPane.INFORMATION_MESSAGE);
}
}
/**
* Thread class which performs the slow accessing of the persistent database with all the saved
* channels and items in the background.
*/
private class OpenDBinBackground extends Thread
{
private static final String THREAD_TITLE = "Load used tags";
private GlobalModel installationModel;
/**
* Create database thread.
*
* @param aInstallationModel model from installer.
*/
OpenDBinBackground(GlobalModel aInstallationModel)
{
super("OpenDBinBackground");
installationModel = aInstallationModel;
setPriority(Thread.MIN_PRIORITY);
}
/**
* Runs the task.
*/
public void run()
{
if (LOG.isLoggable(Level.FINE)) LOG.fine("Loading persistent state from DB.");
ActivityTicket actTicket = ActivityIndicatorView.startOpeningDatabase();
getMainFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
try
{
model.loadingStarted();
final GuidesSet guidesSet = getModel().getGuidesSet();
// Load data into model
IPersistenceManager manager = PersistenceManagerConfig.getManager();
manager.loadGuidesSet(guidesSet);
// Connect persistence listeners
ChangesMonitor changesMonitor = new ChangesMonitor(guidesSet, manager);
domainEventsListener.addDomainListener(changesMonitor);
// Copy guides and preferences from installer model if it is present
if (installationModel != null)
{
GuidesSet installerSet = installationModel.getGuidesSet();
int count = installerSet.getGuidesCount();
for (int i = 0; i < count; i++)
{
guidesSet.add(installerSet.getGuideAt(i));
}
// Copy preferences
Preferences appPrefs = Application.getUserPreferences();
installationModel.storePreferences(appPrefs);
restoreModelPreferencesAndUpdate(model, appPrefs);
}
// If database was reset we need to show recovery options
if (manager.isDatabaseReset())
{
DatabaseRecoverer.performRecovery(model, ApplicationLauncher.getBackupsPath());
}
initMaxViewsAndClickthroughs(guidesSet);
startLoadingUsedTags(guidesSet);
if (dockIconUnreadMonitor != null) dockIconUnreadMonitor.update();
model.loadingFinished();
// Perform sync-on-startup only if the database was OK
if (!manager.isDatabaseReset())
{
// check if it's time for full sync on startup
SyncFull syncFull = new SyncFull(model);
if (syncFull.isSyncTime()) SyncFullAction.getInstance().doSync(null);
}
model.initTransientState();
restoreFeedSelection();
// Extra repainting of highlights to show links to existing feeds correctly
repaintArticlesListHighlights();
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("exception.during.opening.db.in.background"), e);
model.loadingFinished();
} finally
{
getMainFrame().setCursor(Cursor.getDefaultCursor());
ActivityIndicatorView.finishActivity(actTicket);
ApplicationLauncher.enableIPC();
fireInitializationFinished();
checkForWarnings();
startBackgroundProcesses();
autosubscribeIfNecessary();
checkForNewVersion();
}
if (LOG.isLoggable(Level.FINE)) LOG.fine("Done loading persistent state from DB.");
// Force loading of URLs
ServerService.getStartingPointsURL();
}
/**
* Initializes maximum feed views and clickthrough counters with values from guides set.
*
* @param set set.
*/
private void initMaxViewsAndClickthroughs(GuidesSet set)
{
List<IFeed> feeds = set.getFeeds();
for (IFeed feed : feeds)
{
ScoresCalculator.registerMaxClickthroughs(feed.getClickthroughs());
ScoresCalculator.registerMaxFeedViews(feed.getViews());
}
}
/**
* Checks for updates with proactive dialog.
*/
private void checkForNewVersion()
{
if (!ApplicationLauncher.isAutoUpdatesEnabled()) return;
UserPreferences prefs = model.getUserPreferences();
boolean checkForUpdateOnStartup = prefs.isCheckingForUpdatesOnStartup();
if (checkForUpdateOnStartup)
{
// Check for updates as soon as the service becomes available
ConnectionState connectionState = getConnectionState();
connectionState.callWhenServiceIsAvailable(new CheckForNewVersionTask());
}
}
/**
* Starts background loading of used tags.
*
* @param aGuidesSet current guides set.
*/
private void startLoadingUsedTags(final GuidesSet aGuidesSet)
{
Thread loadUsedTagsThread = new Thread(THREAD_TITLE)
{
/** Invoked when running the thread. */
public void run()
{
final TagsRepository repository = TagsRepository.getInstance();
repository.loadFromGuidesSet(aGuidesSet);
UserPreferences prefs = model.getUserPreferences();
final String user = prefs.getTagsDeliciousUser();
final String password = prefs.getTagsDeliciousPassword();
if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(password))
{
try
{
repository.loadTagsFromDelicious(user, password);
} catch (IOException e)
{
LOG.log(Level.WARNING, Strings.error("failed.to.load.used.tags.from.delicious"), e);
}
}
}
};
loadUsedTagsThread.start();
}
/**
* Checks for a new version availability.
*/
private class CheckForNewVersionTask implements Runnable
{
/** Invoked when execution begins. */
public void run()
{
MainFrame mainFrame = getMainFrame();
String currentVersion = ApplicationLauncher.getCurrentVersion();
FullCheckCycle checker = new FullCheckCycle(mainFrame, currentVersion,
false);
try
{
checker.check();
} catch (Throwable e)
{
LOG.log(Level.WARNING, Strings.error("failed.to.finish.updates.check"), e);
}
}
}
}
/**
* Checks if the synchronization is possible.
*
* @return <code>TRUE</code> if possible.
*/
public boolean canSynchronize()
{
FeatureManager fm = getFeatureManager();
boolean can = fm.canSynchronize();
if (!can)
{
List<String> warnings = new ArrayList<String>();
warnings.add(MessageFormat.format(Strings.message("spw.synlimit"),
fm.getSynchronizationsCount(false), fm.getSynchronizationLimit()));
showWarningsDialog(warnings);
}
return can;
}
/**
* Checks subscription limit violation before adding anything capable of
* bringing new subscriptions.
*
* @return <code>TRUE</code> if the violation takes place.
*/
public boolean checkForNewSubscription()
{
return checkForWarnings(false, true, true);
}
/**
* Checks if current feature set limits are violated.
*/
public void checkForWarnings()
{
checkForWarnings(true, true, false);
}
/**
* Checks if current feature set limits are violated.
*
* @param pubL TRUE to check publication limit.
* @param subL TRUE to check subscriptions limit.
* @param eq TRUE to check if the numbers are equal to limits.
*
* @return <code>TRUE</code> if problems detected.
*/
public boolean checkForWarnings(boolean pubL, boolean subL, boolean eq)
{
final List<String> warnings = new ArrayList<String>();
final FeatureManager fm = getFeatureManager();
// Check pub limit
int pubLimit = fm.getPublicationLimit();
if (pubL && pubLimit > -1)
{
int publications = getModel().getGuidesSet().countPublishedGuides();
if (publications > pubLimit || (eq && publications == pubLimit))
{
warnings.add(MessageFormat.format(Strings.message("spw.publimit"), publications, pubLimit));
}
}
// Check sub limit
int subLimit = fm.getSubscriptionLimit();
if (subL && subLimit > -1)
{
int subscriptions = getModel().getGuidesSet().getFeedsList().getFeedsCount();
if (subscriptions > subLimit || (eq && subscriptions == subLimit))
{
warnings.add(MessageFormat.format(Strings.message("spw.sublimit"), subscriptions, subLimit));
}
}
// Show the message if necessary
showWarningsDialog(warnings);
return warnings.size() > 0;
}
/**
* Shows warnings dialog if necessary.
*
* @param warnings warnings.
*/
private void showWarningsDialog(final List<String> warnings)
{
if (warnings.size() > 0)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
PlanWarningsDialog dialog = new PlanWarningsDialog(getMainFrame());
dialog.open(getFeatureManager().getPlanName(), warnings);
}
});
}
}
/**
* Restores the guide and feed selected before the application exit last time.
* Alternatively (if it is the first run, for example) it selects the first found guide.
*/
private void restoreFeedSelection()
{
Preferences prefs = Application.getUserPreferences();
long guideId = prefs.getLong(KEY_SELECTED_GUIDE_ID, -1);
// Find the selected guide by ID or take the first guide if set isn't empty
GuidesSet set = model.getGuidesSet();
IGuide guide = null;
if (guideId != -1)
{
synchronized (set)
{
int count = set.getGuidesCount();
for (int i = 0; guide == null && i < count; i++)
{
IGuide guideItem = set.getGuideAt(i);
if (guideItem.getID() == guideId) guide = guideItem;
}
}
}
if (guide == null && set.getGuidesCount() > 0) guide = set.getGuideAt(0);
// Find selected feed by ID and perform selection
if (guide != null)
{
long feedId = prefs.getLong(KEY_SELECTED_FEED_ID, -1);
IFeed feed = null;
synchronized (guide)
{
int count = guide.getFeedsCount();
for (int i = 0; feed == null && i < count; i++)
{
IFeed feedItem = guide.getFeedAt(i);
if (feedItem.getID() == feedId) feed = feedItem;
}
}
selectGuide(guide, false);
if (feed != null)
{
final IFeed selectionFeed = feed;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
selectFeed(selectionFeed);
}
});
}
}
}
/**
* Saves currently selected feed and guide to restore them later.
*/
private void storeFeedSelection()
{
Preferences prefs = Application.getUserPreferences();
IFeed selectedFeed = model.getSelectedFeed();
IGuide selectedGuide = model.getSelectedGuide();
long guideId = selectedGuide == null ? -1 : selectedGuide.getID();
long feedId = selectedFeed == null ? -1 : selectedFeed.getID();
prefs.putLong(KEY_SELECTED_GUIDE_ID, guideId);
prefs.putLong(KEY_SELECTED_FEED_ID, feedId);
}
/**
* Reads in the last saved persistent state and restores the Model to that state. If there's
* something wrong with the persisted state then we start over with a default state.
*
* @param aModel model of new version if it was detected or <code>null</code> in common case.
*/
public void restorePersistentState(GlobalModel aModel)
{
registerAppCloseEventListener();
new OpenDBinBackground(aModel).start();
if (LOG.isLoggable(Level.FINE)) LOG.fine("Done loading persistent state...");
}
/**
* Register a listener with the JGoodies Application object to be called
* just before app is closed for any reason.
*/
private void registerAppCloseEventListener()
{
// Register listener that is called just before app is closed.
Application.addApplicationListener(new ApplicationAdapter()
{
/**
* Invoked if the application is closing.
*
* @param evt the related <code>ApplicationEvent</code>.
*/
public void applicationClosing(ApplicationEvent evt)
{
prepareToClose(false);
// Force clean application exit
// If we will not do this the Application will continue with useless
// frames and windows disposal procedure which in some cases gets stuck
// making exit not available. As we don't have anything else to do with
// application we simply do clean exit here.
System.exit(0);
}
});
}
/**
* Called when application is closing to store the state of model and preferences.
*
* @param emergencyExit TRUE if it's an emergency exit.
*/
public void prepareToClose(boolean emergencyExit)
{
// Request termination of background processes
backManager.requestExit();
if (initializationFinished)
{
storeFeedSelection();
File backupsDir = new File(ApplicationLauncher.getBackupsPath());
Backups backups = new Backups(backupsDir, LAST_BACKUPS_TO_KEEP);
try
{
backups.saveBackup(model.getGuidesSet());
} catch (IOException e)
{
LOG.log(Level.SEVERE, Strings.error("failed.to.write.backup"), e);
}
if (!emergencyExit) syncOutOnExit();
model.prepareForApplicationExit();
storePreferences();
}
}
/**
* Analyzes the situation with last sync-out dates and feeds counts and decides
* if the change in feeds count is suspicious -- looks like a damage or something
* wrong. If so, the confirmation dialog is given to user.
*/
private void syncOutOnExit()
{
if (!getConnectionState().isServiceAccessible()) return;
SyncOut syncOut = new SyncOut(model);
if (syncOut.isSyncTime())
{
GuidesSet set = model.getGuidesSet();
ServicePreferences servicePreferences = model.getServicePreferences();
int feedsCount = set.countFeeds();
int lastSyncOutFeedsCount = servicePreferences.getLastSyncOutFeedsCount();
boolean synchronizedBefore = servicePreferences.getLastSyncOutDate() != null;
boolean doSync = feedsCount > 0 || synchronizedBefore;
if (doSync && isSuspiciousDifference(feedsCount, lastSyncOutFeedsCount))
{
String message;
if (feedsCount == 0)
{
message = Strings.message("synconexit.clear.the.list.of.your.saved.subscriptions");
} else
{
message = MessageFormat.format(Strings.message("synconexit.make.a.large.change.0.1"),
lastSyncOutFeedsCount, feedsCount);
}
int result = JOptionPane.showConfirmDialog(getMainFrame(),
MessageFormat.format(Strings.message("synconexit.suspicious.sync.text.0"), message),
Strings.message("synconexit.suspicious.sync.title"), JOptionPane.YES_NO_OPTION,
JOptionPane.INFORMATION_MESSAGE);
doSync = result == JOptionPane.YES_OPTION;
}
if (doSync && canSynchronize()) syncOut.doSynchronization(null, false);
}
}
/**
* Returns TRUE if change in feeds number is suspicious.
*
* @param aFeedsCount current feeds count.
* @param aLastSyncOutFeedsCount last sync feeds count.
*
* @return TRUE if looks suspicious.
*/
static boolean isSuspiciousDifference(int aFeedsCount, int aLastSyncOutFeedsCount)
{
return (aFeedsCount == 0 ||
(aLastSyncOutFeedsCount > CHANGE_CHECK_FEEDS_THRESHOLD &&
aLastSyncOutFeedsCount / aFeedsCount > CHANGE_CHECK_DIFF_TIMES));
}
/**
* Restore user prefefences from Preferences file.
*/
void restorePreferences()
{
final Preferences prefs = Application.getUserPreferences();
overridePreferencesWithPlugins(prefs);
// Propagate default preferences to data feeds
DataFeed.setGlobalUpdatePeriod(UserPreferences.DEFAULT_RSS_POLL_MIN *
Constants.MILLIS_IN_MINUTE);
DataFeed.setGlobalPurgeUnread(!UserPreferences.DEFAULT_PRESERVE_UNREAD);
DataFeed.setGlobalPurgeLimit(UserPreferences.DEFAULT_PURGE_COUNT);
UnreadButton.setShowMenuOnClick(UserPreferences.DEFAULT_SHOW_UNREAD_BUTTON_MENU);
restoreModelPreferencesAndUpdate(model, prefs);
}
/**
* Restore moel preferences and updates other components depending on them.
*
* @param mdl model.
* @param prefs preferences object.
*/
public static void restoreModelPreferencesAndUpdate(GlobalModel mdl, Preferences prefs)
{
mdl.restorePreferences(prefs);
// Update all dependent components
ImageBlocker.restorePreferences(prefs);
SentimentsConfig.restorePreferences(prefs);
setProxySettings(mdl.getUserPreferences());
GuideDisplayModeManager.getInstance().restorePreferences(prefs);
FeedDisplayModeManager.getInstance().restorePreferences(prefs);
NotificationArea.setAppIconAlwaysVisible(mdl.getUserPreferences().isShowAppIconInSystray());
PostToBlogAction.update();
FeedLinkPostToBlogAction.update();
}
private void overridePreferencesWithPlugins(Preferences prefs)
{
List<com.salas.bb.plugins.domain.Package> pkgs = Manager.getEnabledPackages();
for (Package pkg : pkgs)
{
for (IPlugin plugin : pkg)
{
if (plugin instanceof AdvancedPreferencesPlugin)
{
AdvancedPreferencesPlugin app = (AdvancedPreferencesPlugin)plugin;
app.overridePreferences(prefs);
}
}
}
}
/**
* Sets property settings from the user preferences.
*
* @param prefs preferences.
*/
static void setProxySettings(UserPreferences prefs)
{
Properties sys = System.getProperties();
if (prefs.isProxyEnabled() && StringUtils.isNotEmpty(prefs.getProxyHost()))
{
String host = prefs.getProxyHost().trim();
String port = Integer.toString(prefs.getProxyPort());
sys.put("http.proxyHost", host);
sys.put("http.proxyPort", port);
sys.put("https.proxyHost", host);
sys.put("https.proxyPort", port);
sys.put("ftp.proxyHost", host);
sys.put("ftp.proxyPort", port);
} else
{
sys.remove("http.proxyHost");
sys.remove("http.proxyPort");
sys.remove("https.proxyHost");
sys.remove("https.proxyPort");
sys.remove("ftp.proxyHost");
sys.remove("ftp.proxyPort");
}
}
/**
* Save User Preferences to Preferences file.
*/
private void storePreferences()
{
final Preferences prefs = Application.getUserPreferences();
// Give mainFrame a chance to save its window position.
mainFrame.prepareToClose();
model.storePreferences(prefs);
GuideDisplayModeManager.getInstance().storePreferences(prefs);
FeedDisplayModeManager.getInstance().storePreferences(prefs);
ImageBlocker.storePreferences(prefs);
SentimentsConfig.storePreferences(prefs);
}
/**
* Exit BlogBridge as a whole.
*/
public static void exitApplication()
{
boolean exitConfirmed = true;
// Check if we have downloads running
int downloads = NetManager.getDownloadsCount();
if (downloads > 0)
{
int res = JOptionPane.showConfirmDialog(SINGLETON.getMainFrame(),
Strings.message("exit.downloads.running"),
Strings.message("exit"),
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE,
Resources.getLargeApplicationIcon());
exitConfirmed = res == JOptionPane.YES_OPTION;
}
if (exitConfirmed) Application.close();
}
/**
* Sets the status of the application or other information to the status bar.
*
* @param status status.
*/
public void setStatus(String status)
{
MainFrame frame = getMainFrame();
if (frame != null) frame.setStatus(status);
}
/**
* Moves feed from one guide to another. It adds feed to the tail of destination guide
* list.
*
* @param feed feed to move.
* @param from old guide.
* @param to new guide.
* @param index new index in destination guide.
*/
public void moveFeed(final IFeed feed, final StandardGuide from,
final StandardGuide to, int index)
{
if (feed == null || from == null || to == null) return;
final UserPreferences prefs = getModel().getUserPreferences();
if (from == to && prefs.isSortingEnabled())
{
// No visible effect
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
Map names = FeedsSortOrder.SORTING_CLASS_NAMES;
String firstSortOrder = (String)names.get(prefs.getSortByClass1());
String secondSortOrder = (String)names.get(prefs.getSortByClass2());
String title = Strings.message("move.feed.title");
String msg = MessageFormat.format(Strings.message("move.feeds.no.effect"),
firstSortOrder, secondSortOrder);
JOptionPane.showMessageDialog(getMainFrame(), msg, title, JOptionPane.INFORMATION_MESSAGE);
}
});
}
from.moveFeed(feed, to, index);
}
/**
* Moves guide to the new position in the list.
*
* @param cg guide to move.
* @param insertPosition index to insert at.
*/
public void moveGuide(final IGuide cg, final int insertPosition)
{
if (cg != null)
{
GlobalModel.SINGLETON.getGuidesSet().relocateGuide(cg, insertPosition);
}
}
public static void pinArticles(boolean pinned, IGuide guide, IFeed feed, IArticle ... articles)
{
if (articles == null || articles.length == 0) return;
int cnt = 0;
// Pin and count
for (IArticle article : articles)
{
if (article.isPinned() != pinned) cnt++;
article.setPinned(pinned);
}
if (pinned && cnt > 0)
{
IPersistenceManager pm = PersistenceManagerConfig.getManager();
pm.getStatisticsManager().articlesPinned(guide, feed, cnt);
}
}
/**
* Marks articles as (un)read in bulk and updates the statistics.
*
* @param read <code>TRUE</code> to mark as read, otherwise -- unread.
* @param guide guide to associate with reading (NULLable).
* @param feed feed to associate with reading (NULLable).
* @param articles articles to mark.
*/
public static void readArticles(boolean read, IGuide guide, IFeed feed, IArticle ... articles)
{
if (articles == null || articles.length == 0) return;
int cnt = 0;
// Mark and count
for (IArticle article : articles)
{
if (article.isRead() != read) cnt++;
article.setRead(read);
}
// Record stats if it's reading and the count is greater than 0
if (read && cnt > 0)
{
IPersistenceManager pm = PersistenceManagerConfig.getManager();
pm.getStatisticsManager().articlesRead(guide, feed, cnt);
}
}
/**
* Marks feeds as (un)read and updates stats in bulk.
*
* @param read <code>TRUE</code> to mark as read, otherwise -- unread.
* @param guide guide to associate with reading (NULLable).
* @param feeds feeds to mark.
*/
public static void readFeeds(boolean read, IGuide guide, IFeed ... feeds)
{
if (feeds == null || feeds.length == 0) return;
for (IFeed feed : feeds)
{
int cnt = 0;
synchronized (feed)
{
if (read) cnt = feed.getUnreadArticlesCount();
feed.setRead(read);
}
if (cnt > 0)
{
IPersistenceManager pm = PersistenceManagerConfig.getManager();
pm.getStatisticsManager().articlesRead(guide, feed, cnt);
}
}
}
/**
* Marks guides a (un)read and updates stats.
*
* @param read <code>TRUE</code> to mark as read, otherwise -- unread.
* @param guides guides to mark.
*/
public static void readGuides(boolean read, IGuide ... guides)
{
if (guides == null || guides.length == 0) return;
for (IGuide guide : guides)
{
IFeed[] feeds = GlobalModel.SINGLETON.getVisibleFeeds(guide);
readFeeds(read, guide, feeds);
}
}
/**
* Adds new domain listener.
*
* @param l domain listener
*/
public void addDomainListener(final IDomainListener l)
{
domainEventsListener.addDomainListener(l);
}
/**
* Removes a domain listener.
*
* @param l listener.
*/
public void removeDomainListener(IDomainListener l)
{
domainEventsListener.removeDomainListener(l);
}
/**
* Adds new controller listener object to the list.
*
* @param l listener object reference.
*/
public void addControllerListener(final IControllerListener l)
{
listeners.add(l);
}
/**
* Removes registration of listener object.
*
* @param l listener object reference.
*/
public void removeControllerListener(final IControllerListener l)
{
listeners.remove(l);
}
/**
* Fires article selection event.
*
* @param article article selected.
*/
public void fireArticleSelected(IArticle article)
{
for (IControllerListener listener : listeners)
{
try
{
listener.articleSelected(article);
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
}
}
}
/**
* Fires event after selection of feed.
*
* @param feed feed to be selected.
*/
public void fireFeedSelected(final IFeed feed)
{
for (IControllerListener listener : listeners)
{
try
{
listener.feedSelected(feed);
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
}
}
}
/**
* Fires guide selection event.
*
* @param guide guide which was selected.
*/
public void fireGuideSelected(final IGuide guide)
{
for (IControllerListener listener : listeners)
{
try
{
listener.guideSelected(guide);
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
}
}
}
/**
* Set GlobalController flag that says initializion is finished, and
* fires initialization finish event.
*/
public void fireInitializationFinished()
{
this.initializationFinished = true;
for (IControllerListener listener : listeners)
{
try
{
listener.initializationFinished();
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
}
}
}
/**
* Returns the initialization flag.
*
* @return TRUE if initialization finished.
*/
public boolean isInitializationFinished()
{
return initializationFinished;
}
/**
* Adds new guide at the specified position.
*
* @param title title of the guide.
* @param iconKey key of icon.
* @param autoFeedDiscovery auto feed discovery flag.
*
* @return created guide or NULL if failed.
*/
public StandardGuide createStandardGuide(String title, String iconKey,
boolean autoFeedDiscovery)
{
StandardGuide cg = new StandardGuide();
cg.setTitle(title);
cg.setIconKey(iconKey);
cg.setAutoFeedsDiscovery(autoFeedDiscovery);
final GuidesSet guidesSet = getModel().getGuidesSet();
IGuide selectedGuide = getModel().getSelectedGuide();
guidesSet.add(selectedGuide == null ? -1 : guidesSet.indexOf(selectedGuide), cg);
return cg;
}
/**
* Adds new channel to the currently selected guide after currently selected channel.
*
* @param url URL to use for addition or separated list of URL's.
* @param forceQuery TRUE to open dialog for URL querying.
*
* @return new feed (the first from the list in multi-mode) or
* NULL if selected guide isn't Standard Guide or URL's weren't specified.
*/
public DirectFeed createDirectFeed(String url, boolean forceQuery)
{
DirectFeed feed = null;
IGuide guide = model.getSelectedGuide();
if (guide == null || guide instanceof StandardGuide)
{
if (url == null || forceQuery)
{
ValueHolder urlHolder = new ValueHolder(url);
AddDirectFeedDialog dialog = new AddDirectFeedDialog(getMainFrame(), urlHolder);
dialog.open();
url = dialog.hasBeenCanceled() ? null : (String)urlHolder.getValue();
}
Set<String> urls = parseMultiURL(url);
DirectFeed[] feeds = createDirectFeeds(urls, (StandardGuide)guide);
if (feeds.length > 0) feed = feeds[0];
}
return feed;
}
/**
* Parses string with multiple URL's delimitered by URL separator char defined in
* constants. The set of URL's returned may be empty if source multi-URL was NULL
* or empty string. The resulting set contains no duplicate URL's and all URL's in �
* it are already "fixed".
*
* @param multiURL multi-URL or NULL.
*
* @return set of unique fixed URL's.
*
* @see Constants#URL_SEPARATOR
* @see com.salas.bb.utils.StringUtils#fixURL(String)
*/
static Set<String> parseMultiURL(String multiURL)
{
Set<String> urls = new HashSet<String>();
if (multiURL != null)
{
StringTokenizer st = new StringTokenizer(multiURL, Constants.URL_SEPARATOR);
while (st.hasMoreTokens()) urls.add(StringUtils.fixURL(st.nextToken()));
}
return urls;
}
/**
* Creates direct feeds out of list of URL's and assigns them to the guide.
*
* @param urls set of URL's.
* @param guide guide to assign new feeds to.
*
* @return list of created feeds.
*/
public DirectFeed[] createDirectFeeds(Set<String> urls, StandardGuide guide)
{
DirectFeed[] feeds;
if (urls == null)
{
feeds = new DirectFeed[0];
} else
{
int count = urls.size();
feeds = new DirectFeed[count];
int i = 0;
for (String url : urls)
{
try
{
feeds[i++] = createDirectFeed(guide, StringUtils.fixURL(url));
} catch (MalformedURLException e)
{
JOptionPane.showMessageDialog(getMainFrame(),
Strings.message("invalid.url.message"),
Strings.message("invalid.url"),
JOptionPane.WARNING_MESSAGE);
LOG.warning(MessageFormat.format(Strings.error("invalid.url"), url));
}
}
}
return feeds;
}
/**
* Adds new direct feed to the guide by specified reference. Meta-data is queried from
* repository and passed to feed.
*
* @param guide guide.
* @param reference reference text for feed discovery.
*
* @return new feed or <code>NULL</code> if wasn't added.
* @throws java.net.MalformedURLException in case of bad URL.
*/
public DirectFeed createDirectFeed(IGuide guide, String reference)
throws MalformedURLException
{
return createDirectFeed(guide, new URL(reference));
}
/**
* Creates direct feed and adds it to the guide.
*
* @param guide guide or <code>NULL</code> for the first guide or new.
* @param aUrl URL to load feed from.
*
* @return new feed or <code>NULL</code> if wasn't added.
*/
public DirectFeed createDirectFeed(IGuide guide, URL aUrl)
{
if (aUrl == null) return null;
String reference = aUrl.toString();
if (guide == null) guide = chooseOrMakeGuideForNewFeed();
DirectFeed feed;
boolean proceed = true;
boolean existing = false;
DirectFeed otherFeed = model.getGuidesSet().getFeedsList().findDirectFeed(aUrl, true);
if (otherFeed != null && otherFeed.isInitialized())
{
// There's another feed with the same URL, we reuse it
feed = otherFeed;
// If that other feed is not in the guide we are adding this
// duplicate to, we still add a feed. If it's in the same guide,
// we don't.
if (guide.indexOf(feed) == -1) guide.add(feed);
return feed;
} else
{
feed = new DirectFeed();
feed.setBaseTitle(reference);
FeedMetaDataHolder metaData = metaDataManager.lookupOrDiscover(aUrl);
feed.setMetaData(metaData);
if (metaData.isComplete() || metaData.isDiscoveredInvalid())
{
if (metaData.isDiscoveredInvalid())
{
proceed = !processInvalidDiscovery(metaData, feed, aUrl);
}
// Checking again because after invalid discovery processing it may change
if (metaData.isDiscoveredValid())
{
URL xmlURL = metaData.getXmlURL();
GuidesSet set = getModel().getGuidesSet();
DirectFeed existingFeed = set.findDirectFeed(xmlURL);
if (existingFeed != null)
{
feed = existingFeed;
existing = true;
} else feed.setXmlURL(xmlURL);
}
// This blog is already discovered so repaint highlights
if (proceed && !existing) repaintArticlesListHighlights();
}
}
if (proceed) guide.add(feed);
if (!existing)
{
if (proceed)
{
poller.update(feed, true, true);
} else feed = null;
} else checkForDuplicates(feed);
return feed;
}
/**
* Checks for duplicate feed present, warns a user and removes the feed if user wishes to.
*
* @param aFeed feed.
*/
private void checkForDuplicates(DirectFeed aFeed)
{
IGuide[] allGuides = aFeed.getParentGuides();
if (allGuides.length > 1)
{
ShowDuplicateFeeds dialog = new ShowDuplicateFeeds(mainFrame, aFeed);
dialog.open();
IGuide[] removals = dialog.getRemovals();
if (removals != null)
{
for (IGuide removal : removals) removal.remove(aFeed);
}
}
}
/**
* Finds the best title of all.
*
* @param feeds feeds to look for the best title among.
*
* @return title.
*/
static String findBestTitle(NetworkFeed[] feeds)
{
String title = null;
for (NetworkFeed nfeed : feeds)
{
String ntitle = nfeed.getTitle();
if (title == null || (ntitle != null && !ntitle.matches("^[^:]{3,5}://.+")))
{
title = ntitle;
}
}
return title;
}
/**
* Creates query feed and adds it to selected, first or new guide.
*
* @param guide guide to assign this new feed to
* (or <code>NULL</code> to choose or make it)
* @param title title for new feed.
* @param queryType query type.
* @param parameter query parameter.
* @param purgeLimit purge limit.
*
* @return query feed added.
*
*/
public QueryFeed createQueryFeed(StandardGuide guide, String title, int queryType,
String parameter, int purgeLimit)
{
if (guide == null) guide = chooseOrMakeGuideForNewFeed();
GuidesSet set = getModel().getGuidesSet();
QueryType type = QueryType.getQueryType(queryType);
QueryFeed queryFeed = set.findQueryFeed(type, parameter);
if (queryFeed == null)
{
queryFeed = new QueryFeed();
queryFeed.setBaseTitle(title);
queryFeed.setQueryType(type);
queryFeed.setParameter(parameter);
queryFeed.setPurgeLimit(purgeLimit);
} else
{
// TODO !!! display notification about duplicate or continue cleanly?
}
guide.add(queryFeed);
poller.update(queryFeed, true, true);
return queryFeed;
}
/**
* Creates search feed and adds it to the guide or new automatically created guide.
*
* @param aGuide guide to add feed to.
* @param aTitle title of the feed.
* @param aSearchQuery query of the feed.
* @param aPurgeLimit purge limit.
*
* @return newly created feed.
*/
public SearchFeed createSearchFeed(StandardGuide aGuide, String aTitle,
Query aSearchQuery, int aPurgeLimit)
{
if (aGuide == null) aGuide = chooseOrMakeGuideForNewFeed();
GuidesSet set = getModel().getGuidesSet();
SearchFeed searchFeed = set.findSearchFeed(aSearchQuery);
if (searchFeed == null)
{
searchFeed = new SearchFeed();
searchFeed.setBaseTitle(aTitle);
searchFeed.setArticlesLimit(aPurgeLimit);
searchFeed.setQuery(aSearchQuery);
} else
{
// TODO !!! display notification about duplicate or continue cleanly?
}
aGuide.add(searchFeed);
return searchFeed;
}
/**
* Runs a thread updating the given search feed.
*
* @param sfeed feed.
*/
public void updateSearchFeed(final SearchFeed sfeed)
{
if (sfeed == null) return;
new Thread(THREAD_NAME_SEARCH_QUERY)
{
public void run()
{
searchFeedsManager.runQuery(sfeed);
}
}.start();
}
/**
* Chooses guide from existing or creates new guide if none present for a new feed.
* <ul>
* <li>If the guide is currently selected then it will be chosen.</li>
* <li>If it's there's no selected guide, but there are guides in the set,
* the first from them is chosen.</li>
* <li>If there are no guides, the new one is created with default name.</li>
* </ul>
*
* @return guide.
*
* @see #AUTO_GUIDE_TITLE
*/
private StandardGuide chooseOrMakeGuideForNewFeed()
{
IGuide guide = model.getSelectedGuide();
if (guide == null)
{
// If guide isn't selected then select first guide from set or create new one.
GuidesSet guidesSet = getModel().getGuidesSet();
if (guidesSet.getGuidesCount() == 0)
{
guide = createStandardGuide(AUTO_GUIDE_TITLE, null, false);
// If guide wasn't created -- report
if (guide == null) LOG.severe(Strings.error("failed.to.automatically.create.a.new.guide"));
} else
{
guide = guidesSet.getGuideAt(0);
}
}
if (guide == null) throw new RuntimeException(Strings.error("failed.to.create.new.guide"));
// TODO for now we have only StandardGuide's
return (StandardGuide)guide;
}
/**
* Repaints all highlights in articles list immediately.
*/
public void repaintArticlesListHighlights()
{
MainFrame frame = getMainFrame();
if (frame != null) frame.repaintArticlesListHighlights();
}
/**
* Adds the feed with defined dataUrl to the specified position in guide. The meta-data
* repository will not be questioned for discovery, but will be questioned for existing
* meta-data object. If there's no meta-data yet, then the object will be created.
*
* @param guide guide.
* @param dataUrl data URL.
* @param aList reading list.
*
* @return new feed object.
*/
public DirectFeed addDirectFeed(StandardGuide guide, URL dataUrl, ReadingList aList)
{
if (dataUrl == null) throw new NullPointerException(Strings.error("unspecified.data.url"));
DirectFeed feed = getModel().getGuidesSet().findDirectFeed(dataUrl);
if (feed == null)
{
feed = new DirectFeed();
feed.setXmlURL(dataUrl);
}
if (aList != null) aList.add(feed); else guide.add(feed);
return feed;
}
/**
* Updates the feed immediately if the feed is discovered according to its meta-data.
*
* @param feed feed.
*/
public void updateIfDiscovered(DirectFeed feed)
{
FeedMetaDataHolder md = feed.getMetaDataHolder();
if (md != null && md.isDiscoveredValid())
{
poller.update(feed, false);
}
}
/**
* Removes feed from the holder guide, but not from the storage. Notifies model.
* This method is useful when removing feeds which are not added to the db yet
* (undiscovered or invalid).
*
* @param feed feed to remove.
*/
public void deleteNonPersistentFeed(IFeed feed)
{
IGuide[] guides = feed.getParentGuides();
if (guides.length != 0)
{
if (feed instanceof DirectFeed) ((DirectFeed)feed).setMetaData(null);
for (IGuide guide : guides) guide.remove(feed);
}
}
/**
* Starts discovery of link from article's text.
*
* @param url link.
*
* @return meta-data of channel.
*/
public FeedMetaDataHolder discoverLinkFromArticle(URL url)
{
return metaDataManager.lookup(url);
}
/**
* Discovers meta-data for the link.
*
* @param link link.
*
* @return meta-data object.
*/
public FeedMetaDataHolder discover(URL link)
{
return metaDataManager.lookupOrDiscover(link);
}
/**
* Schedules the discovery of feeds in all feeds of the guide.
*
* @param guide guide to analyze.
*/
public void discoverFeedsIn(IGuide guide)
{
IFeed[] feeds = guide.getFeeds();
for (IFeed feed : feeds) discoverFeedsIn(feed);
}
/**
* Schedules the discovery of feeds in all articles of the feed.
*
* @param feed feed to analyze.
*/
public void discoverFeedsIn(IFeed feed)
{
if (feed == null) return;
IArticle[] articles = feed.getArticles();
for (IArticle article : articles) discoverFeedsIn(article, feed);
}
/**
* Creates requested-by string from feed and its holder guide.
*
* @param feed feed.
*
* @return string.
*/
public static String prepareRequestedByFromFeed(IFeed feed)
{
StringBuffer buf = new StringBuffer();
IGuide[] guides = feed.getParentGuides();
if (guides.length > 0) buf.append(GuidesUtils.getGuidesNames(guides)).append(" / ");
buf.append(feed.getTitle());
return buf.toString();
}
/**
* Schedules the discovery of feeds met in the article.
*
* @param article article with links.
* @param feed feed - source of request to record in new meta-data wrappers.
*/
public void discoverFeedsIn(IArticle article, IFeed feed)
{
String requestedBy = prepareRequestedByFromFeed(feed);
// Find the base URL for resolution of relative links
URL baseUrl = article.getLink();
if (baseUrl == null && feed instanceof DirectFeed) baseUrl = ((DirectFeed)feed).getXmlURL();
// Find all links in the article text
Collection<String> links = article.getLinks();
if (links == null) return;
// Request the discovery of all links found one by one
for (String link : links)
{
try
{
FeedMetaDataHolder cmd = discover(new URL(baseUrl, link));
if (cmd.getRequestedBy() == null) cmd.setRequestedBy(requestedBy);
} catch (MalformedURLException e)
{
// No problems about that. Just invalid link in the article.
}
}
}
/**
* When it happens that the reference was not discovered we need to
* tell user about it and give him an opportunity to change his original
* reference or suggest correct data URL.
*
* @param holder holder of meta-data.
* @param feed feed corresponding to this wrapper.
* @param originalURL original URL used to discover meta-data.
*
* @return <code>TRUE</code> if no processing was done.
*/
private boolean processInvalidDiscovery(FeedMetaDataHolder holder,
final DirectFeed feed, URL originalURL)
{
// we need to ask what to do with current discovery:
// 1. leave as is - invalid
// 2. enter the other URL
// 3. suggest correct data URL
feed.setInvalidnessReason(Strings.message("feed.invalidness.reason.undiscovered"));
boolean processingDone = false;
// If the feed is a part of reading list we can't modify or remove it
if (feed.getReadingLists().length > 0) return processingDone;
InvalidDiscoveryDialog dialog = createDialog();
dialog.setTitle(Strings.message("subscription.error.dialog.title"));
URL newDiscoveryUrl = originalURL;
String suggestedUrl = null;
boolean localReference = MDDiscoveryRequest.isLocalURL(originalURL);
boolean inputAccepted = false;
while (!inputAccepted)
{
GlobalController.IDDResult res = openDialog(dialog, newDiscoveryUrl, suggestedUrl, localReference);
int option = res.option;
inputAccepted = true;
if (!res.hasBeenCanceled)
{
newDiscoveryUrl = res.newDiscoveryURL;
suggestedUrl = res.newSuggestionURL;
if (option == InvalidDiscoveryDialog.OPTION_NEW_DISCOVERY)
{
inputAccepted = false;
// Check if input is accepted
if (newDiscoveryUrl != null)
{
// Initiate new discovery
processingDone = true;
inputAccepted = true;
IGuide[] guides = feed.getParentGuides();
deleteNonPersistentFeed(feed);
if (guides != null && guides.length > 0)
{
DirectFeed newFeed = null;
for (IGuide guide : guides)
{
if (newFeed == null)
{
newFeed = createDirectFeed(guide, newDiscoveryUrl);
} else
{
guide.add(newFeed);
}
}
} else
{
createDirectFeed(getModel().getSelectedGuide(), newDiscoveryUrl);
}
}
} else if (option == InvalidDiscoveryDialog.OPTION_SUGGEST_URL)
{
// Check URL and suggest if it's recognizable
try
{
// Check if we have correct URL
URL url = new URL(StringUtils.fixURL(suggestedUrl));
DirectDiscoverer dd = new DirectDiscoverer();
DiscoveryResult result = dd.discover(url);
if (result != null)
{
ServerService.metaSuggestFeedUrl(originalURL.toString(), suggestedUrl);
// set newly discovered URL and mark the data as no longer invalid
holder.setXmlURL(url);
holder.setInvalid(false);
// reset processing flag to continue as if nothing happened
processingDone = false;
} else
{
// The specified url isn't pointing to the feed
inputAccepted = false;
}
} catch (MalformedURLException e)
{
// User supplied malformed URL -- too bad
inputAccepted = false;
} catch (UrlDiscovererException e)
{
// Possibly communication error
inputAccepted = false;
}
} else if (option == InvalidDiscoveryDialog.OPTION_CANCEL)
{
// Remove invalid feed
processingDone = true;
deleteNonPersistentFeed(feed);
}
}
}
return processingDone;
}
/**
* Temporary dialog results holder.
*/
private static class IDDResult
{
private int option;
private boolean hasBeenCanceled;
private URL newDiscoveryURL;
private String newSuggestionURL;
}
/**
* Opens the dialog in EDT and returns the package of results.
*
* @param dialog dialog to open.
* @param newDiscoveryUrl new discovery URL.
* @param suggestedUrl current suggestion.
* @param localReference <code>TRUE</code> when a link is local.
*
* @return results.
*/
private static IDDResult openDialog(final InvalidDiscoveryDialog dialog, final URL newDiscoveryUrl,
final String suggestedUrl, final boolean localReference)
{
final IDDResult result = new IDDResult();
Runnable task = new Runnable()
{
public void run()
{
result.option = dialog.open(newDiscoveryUrl, suggestedUrl, !localReference);
result.hasBeenCanceled = dialog.hasBeenCanceled();
result.newDiscoveryURL = dialog.getNewDiscoveryUrl();
result.newSuggestionURL = dialog.getSuggestedFeedUrl();
}
};
UifUtilities.invokeAndWait(task, "Failed to open dialog.", Level.SEVERE);
return result;
}
/**
* Creating invalid discovery dialog in EDT.
*
* @return dialog.
*/
private InvalidDiscoveryDialog createDialog()
{
final ValueHolder vh = new ValueHolder();
Runnable task = new Runnable()
{
public void run()
{
InvalidDiscoveryDialog dialog = new InvalidDiscoveryDialog(getMainFrame());
synchronized (vh)
{
vh.setValue(dialog);
}
}
};
UifUtilities.invokeAndWait(task, "Failed to create invalid discovery dialog.", Level.SEVERE);
return (InvalidDiscoveryDialog)vh.getValue();
}
/**
* Registeres currently hovered link.
*
* @param link hovered link or <code>NULL</code>.
*/
public void setHoveredHyperLink(URL link)
{
if (hoveredLink != link)
{
hoveredLink = link;
setStatus(link == null ? "" : link.toString());
}
}
/**
* Returns hovered hyper-link URL or <code>NULL</code>.
*
* @return link or <code>NULL</code>.
*/
public URL getHoveredHyperLink()
{
return hoveredLink;
}
/**
* Get the feed by currently hovered hyper-link URL.
*
* @return feed or <code>NULL</code> if not found.
*/
public NetworkFeed getFeedByHoveredHyperLink()
{
final URL url = getHoveredHyperLink();
NetworkFeed feed = null;
if (url != null)
{
FeedMetaDataHolder metaData = discoverLinkFromArticle(url);
if (metaData != null)
{
URL xmlURL = metaData.getXmlURL();
feed = getModel().getGuidesSet().findDirectFeed(xmlURL);
}
}
return feed;
}
/**
* Returns list of selected guides.
*
* @return guides.
*/
public IGuide[] getSelectedGuides()
{
GuidesList guidesList = getMainFrame().getGudiesPanel().getGuidesList();
Object[] guidesO = guidesList.getSelectedValues();
IGuide[] guides = new IGuide[guidesO.length];
for (int i = 0; i < guidesO.length; i++)
{
guides[i] = (IGuide)guidesO[i];
}
return guides;
}
/**
* Returns list of selected feeds.
*
* @return feeds.
*/
public IFeed[] getSelectedFeeds()
{
JList feedsList = getMainFrame().getFeedsPanel().getFeedsList();
Object[] feedsO = feedsList.getSelectedValues();
IFeed[] feeds = new IFeed[feedsO.length];
for (int i = 0; i < feedsO.length; i++)
{
feeds[i] = (IFeed)feedsO[i];
}
return feeds;
}
/**
* Shows new publishing dialog only if it's enabled.
*/
public void showNewPublishingDialog()
{
UserPreferences prefs = model.getUserPreferences();
if (prefs.isShowingNewPubAlert())
{
NewPublicationDialog newPublicationDialog = new NewPublicationDialog(getMainFrame());
newPublicationDialog.open();
prefs.setShowingNewPubAlert(!newPublicationDialog.isDoNotShowAgain());
if (!newPublicationDialog.hasBeenCanceled())
{
SyncFullAction.getInstance().actionPerformed(null);
}
}
}
/**
* Adapts navigation to the controller.
*/
private class NavigatorAdapter implements IArticleListNavigationListener
{
/**
* Invoked when list component notifies model about that there's no articles left to switch
* on and next feed required.
*
* @param mode of selecting next feed.
*
* @see com.salas.bb.views.INavigationModes#MODE_NORMAL
* @see com.salas.bb.views.INavigationModes#MODE_UNREAD
*/
public void nextFeed(int mode)
{
NavigatorAdv.NavigationInfoKey key;
switch (mode)
{
case INavigationModes.MODE_NORMAL:
key = NavigatorAdv.NavigationInfoKey.NEXT;
break;
case INavigationModes.MODE_UNREAD:
key = NavigatorAdv.NavigationInfoKey.NEXT_UNREAD;
break;
default:
key = null;
break;
}
if (key == null)
{
LOG.severe(MessageFormat.format(Strings.error("invalid.next.mode"), mode));
} else
{
NavigatorAdv.Destination dest = navigator.getDestination(key);
selectDestination(dest, true, mode);
}
}
/**
* Invoked when list component notifies model about that there's no articles left to switch
* on and previous feed required.
*
* @param mode of selecting next feed.
*
* @see com.salas.bb.views.INavigationModes#MODE_NORMAL
* @see com.salas.bb.views.INavigationModes#MODE_UNREAD
*/
public void prevFeed(int mode)
{
NavigatorAdv.NavigationInfoKey key;
switch (mode)
{
case INavigationModes.MODE_NORMAL:
key = NavigatorAdv.NavigationInfoKey.PREV;
break;
case INavigationModes.MODE_UNREAD:
key = NavigatorAdv.NavigationInfoKey.PREV_UNREAD;
break;
default:
key = null;
break;
}
if (key == null)
{
LOG.severe(MessageFormat.format(Strings.error("invalid.prev.mode"), mode));
} else
{
NavigatorAdv.Destination dest = navigator.getDestination(key);
selectDestination(dest, false, mode);
}
}
private void selectDestination(final NavigatorAdv.Destination dest,
final boolean next, final int mode)
{
if (dest == null)
{
if (getModel().getUserPreferences().isSoundOnNoUnread()) Sound.play("sound.no.unread");
return;
}
final IGuide guide = dest.guide;
final IFeed feed = dest.feed;
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Next: Guide=" + guide.getTitle() + " Feed=" + feed.getTitle());
}
Runnable task = new Runnable()
{
public void run()
{
selectGuide(guide, false);
selectFeed(feed);
final MainFrame frame = getMainFrame();
final IFeedDisplay feedDisplay = frame.getArticlesListPanel().getFeedView();
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
if (next)
{
feedDisplay.selectFirstArticle(mode);
} else
{
feedDisplay.selectLastArticle(mode);
}
}
});
}
};
if (UifUtilities.isEDT()) task.run(); else SwingUtilities.invokeLater(task);
}
}
/**
* Listens for discovery events.
*/
private class DiscoveryListener implements IDiscoveryListener
{
private static final int MAX_CHARS_IN_TOOLTIP_REFERENCE = 40;
private Map<URL, ActivityTicket> wrapperToTicket = new IdentityHashMap<URL, ActivityTicket>();
/**
* Invoked when discovery of some meta-data object started.
*
* @param url URL being discovered.
*/
public void discoveryStarted(URL url)
{
String urlString = url.toString();
// Get activity ticket
if (urlString.length() > MAX_CHARS_IN_TOOLTIP_REFERENCE)
{
urlString = urlString.substring(0, MAX_CHARS_IN_TOOLTIP_REFERENCE) + "\u2026";
}
wrapperToTicket.put(url, ActivityIndicatorView.startDiscovery(urlString));
}
/**
* Invoked when discovery of some meta-data object successfully finished.
*
* @param url URL has been discovered.
* @param complete <code>TRUE</code> when discovery is complete and there will be no
* rediscovery scheduled.
*/
public void discoveryFinished(URL url, boolean complete)
{
finishDiscoveryIndication(url);
if (!complete) return;
FeedMetaDataHolder holder = metaDataManager.lookup(url);
DirectFeed[] feeds = findFeedsWatchingMetaData(holder);
if (feeds.length > 0)
{
URL xmlUrl = holder.getXmlURL();
DirectFeed existingFeed = xmlUrl == null ? null : getModel().getGuidesSet().findDirectFeed(xmlUrl);
for (DirectFeed feed : feeds)
{
boolean proceed = true;
boolean existing = false;
// If this feed is not new (just rediscovery), continue
if ((existingFeed == null || existingFeed == feed) && feed.isInitialized())
{
existingFeed = feed;
continue;
}
if (holder.isDiscoveredInvalid() && !feed.isInitialized())
{
proceed = !processInvalidDiscovery(holder, feed, url);
}
if (holder.isDiscoveredValid())
{
// Discovered correctly
if (existingFeed != null)
{
if (existingFeed != feed)
{
IFeed selectedFeed = getModel().getSelectedFeed();
boolean thisFeedSelected = feed == selectedFeed;
GuidesSet.replaceFeed(feed, existingFeed);
if (thisFeedSelected)
{
final IFeed selectFeed = existingFeed;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
selectFeed(selectFeed);
}
});
}
existing = true;
}
} else
{
existingFeed = feed;
feed.setXmlURL(xmlUrl);
}
}
if (existing && !feed.isInitialized()) checkForDuplicates(feed);
if (!existing && proceed) updateIfDiscovered(feed);
}
}
// Update all highlights to reflect new state of the link
if (holder.isDiscoveredValid()) repaintArticlesListHighlights();
}
/**
* Returns the list of all feeds watching given holder.
*
* @param aHolder holder.
*
* @return feeds.
*/
private DirectFeed[] findFeedsWatchingMetaData(FeedMetaDataHolder aHolder)
{
List<DirectFeed> watchers = new ArrayList<DirectFeed>();
List<IFeed> feeds = model.getGuidesSet().getFeeds();
for (IFeed feed : feeds)
{
if (feed instanceof DirectFeed)
{
DirectFeed dfeed = (DirectFeed)feed;
if (dfeed.getMetaDataHolder() == aHolder) watchers.add(dfeed);
}
}
return watchers.toArray(new DirectFeed[watchers.size()]);
}
/**
* Invoked when discovery of some meta-data object failed.
*
* @param url URL has been failed to discover.
*/
public void discoveryFailed(URL url)
{
finishDiscoveryIndication(url);
}
/**
* Finishes indication of discovery of given URL.
*
* @param url URL.
*/
private void finishDiscoveryIndication(URL url)
{
final ActivityTicket ticket = wrapperToTicket.get(url);
if (ticket != null)
{
ActivityIndicatorView.finishActivity(ticket);
wrapperToTicket.remove(url);
}
}
}
/**
* Listens to events from selected feed.
*/
private class SelectedFeedListener extends FeedAdapter
{
/**
* Called when some article is added to the feed.
*
* @param feed feed.
* @param article article.
*/
public void articleAdded(IFeed feed, IArticle article)
{
for (IControllerListener listener : listeners)
{
try
{
listener.articleAdded(article, feed);
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
}
}
}
/**
* Called when some article is removed from the feed.
*
* @param feed feed.
* @param article article.
*/
public void articleRemoved(IFeed feed, IArticle article)
{
for (IControllerListener listener : listeners)
{
try
{
listener.articleRemoved(article, feed);
} catch (Exception e)
{
LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
}
}
}
/**
* Called when information in feed changed.
*
* @param feed feed.
* @param property property of the feed.
* @param oldValue old property value.
* @param newValue new property value.
*/
public void propertyChanged(final IFeed feed, String property, Object oldValue, Object newValue)
{
if (IFeed.PROP_TITLE.equals(property))
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
getMainFrame().updateTitle(feed);
}
});
} else if (SearchFeed.PROP_QUERY.equals(property) && model.getSelectedFeed() == feed)
{
if (updateSearchHighlights(feed, feed)) repaintArticlesListHighlights();
}
}
}
/**
* Takes credentials from service preferences.
*/
private class BBServiceCredentialCallback implements ICredentialsCallback
{
/**
* Invoked when storage needs to know current user name.
*
* @return user name or <code>NULL</code> if service is disabled.
*/
public String getUserName()
{
String userName = null;
if (model != null)
{
userName = model.getServicePreferences().getEmail();
if (StringUtils.isEmpty(userName)) userName = null;
}
return userName;
}
/**
* Invoked when storage needs to know current user password.
*
* @return user password or <code>NULL</code> if service is disabled.
*/
public String getUserPassword()
{
String password = null;
if (model != null)
{
password = model.getServicePreferences().getPassword();
if (StringUtils.isEmpty(password)) password = null;
}
return password;
}
}
/**
* Takes credentials from user preferences.
*/
private class UserPreferencesCallback implements ICredentialsCallback
{
/**
* Invoked when service handler needs to know current user name.
*
* @return user name or <code>NULL</code> if service is disabled.
*/
public String getUserName()
{
String userName = null;
if (model != null)
{
userName = model.getUserPreferences().getTagsDeliciousUser();
if (StringUtils.isEmpty(userName)) userName = null;
}
return userName;
}
/**
* Invoked when service handler needs to know current user password.
*
* @return user password or <code>NULL</code> if service is disabled.
*/
public String getUserPassword()
{
String password = null;
if (model != null)
{
password = model.getUserPreferences().getTagsDeliciousPassword();
if (StringUtils.isEmpty(password)) password = null;
}
return password;
}
}
/**
* Selects the guide.
*/
private class SelectGuideTask implements Runnable
{
private final IGuide guide;
private final boolean selectFeed;
private final boolean alreadySelected;
/**
* Creates task to select the guide.
*
* @param aGuide guide to select.
* @param aSelectFeed <code>TRUE</code> to select feed.
* @param aAlreadySelected <code>TRUE</code> if guide is already selected and only event firing required.
*/
public SelectGuideTask(IGuide aGuide, boolean aSelectFeed, boolean aAlreadySelected)
{
guide = aGuide;
selectFeed = aSelectFeed;
alreadySelected = aAlreadySelected;
}
/**
* Selects guide, fires event and selects feed (if necessary).
*/
public void run()
{
if (!alreadySelected) model.setSelectedGuide(guide);
// We need to fire this event anyway because the name of initially
// selected guide should appear in the headers
fireGuideSelected(guide);
if (!alreadySelected)
{
IFeed feed = null;
int gsm = model.getUserPreferences().getGuideSelectionMode();
if (selectFeed && gsm != UserPreferences.GSM_NO_FEED)
{
feed = gsm == UserPreferences.GSM_FIRST_FEED
? model.getGuideModel().getSize() == 0
? null : (IFeed)model.getGuideModel().getElementAt(0)
: model.getSelectedFeed();
}
selectFeed(feed);
}
// STATS: Report the guide selection
PersistenceManagerConfig.getManager().getStatisticsManager().guideVisited(guide);
}
}
}