Package com.salas.bb.core

Source Code of com.salas.bb.core.GlobalController$OpenDBinBackground$CheckForNewVersionTask

// 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);
        }
    }
}
TOP

Related Classes of com.salas.bb.core.GlobalController$OpenDBinBackground$CheckForNewVersionTask

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.