// 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: SyncOut.java,v 1.47 2008/02/28 15:59:50 spyromus Exp $ // package com.salas.bb.service.sync; import com.salas.bb.core.FeedDisplayModeManager; import com.salas.bb.core.GlobalController; import com.salas.bb.core.GlobalModel; import com.salas.bb.domain.*; import com.salas.bb.domain.prefs.StarzPreferences; import com.salas.bb.domain.prefs.UserPreferences; import com.salas.bb.imageblocker.ImageBlocker; import com.salas.bb.plugins.Manager; import com.salas.bb.sentiments.SentimentsConfig; import com.salas.bb.service.ServerService; import com.salas.bb.service.ServerServiceException; import com.salas.bb.service.ServicePreferences; import com.salas.bb.twitter.TwitterPreferences; import com.salas.bb.utils.StringUtils; import com.salas.bb.utils.i18n.Strings; import com.salas.bb.utils.opml.Converter; import com.salas.bb.utils.uif.UifUtilities; import com.salas.bb.views.settings.FeedRenderingSettings; import com.salas.bb.views.settings.RenderingSettingsNames; import com.salas.bbutilities.opml.export.Exporter; import com.salas.bbutilities.opml.objects.OPMLGuideSet; import com.salas.bbutilities.opml.utils.Transformation; import org.jdom.Document; import java.awt.*; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.text.MessageFormat; import java.util.*; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * Outgoing synchronization. */ public class SyncOut extends AbstractSynchronization { private static final Logger LOG = Logger.getLogger(SyncOut.class.getName()); private static final String THREAD_NAME_PING = "Ping RL"; private static final MessageFormat RL_PUB_URL = new MessageFormat("http://www.blogbridge.com/rl/{0,number,#}/{1}.opml"); /** * Creates outgoing synchronization module. * * @param aModel model to operate. */ public SyncOut(GlobalModel aModel) { super(aModel); } /** * Performs the step-by-step synchronization and collects stats. * * @param progress listener to notify. * @param aEmail email of user account. * @param aPassword password of user account. * * @return statistics. */ protected Stats doSynchronization(IProgressListener progress, String aEmail, String aPassword) { SyncOutStats stats = new SyncOutStats(); try { // save feeds if (servicePreferences.isSyncFeeds()) { if (progress != null) progress.processStep(Strings.message("service.sync.out.saving.guides.and.feeds")); storeFeeds(aEmail, aPassword, stats); if (progress != null) progress.processStepCompleted(); } // initiate background ping pingGuides(); // save preferences if (servicePreferences.isSyncPreferences()) { if (progress != null) progress.processStep(Strings.message("service.sync.out.saving.preferences")); storePreferences(aEmail, aPassword, stats); if (progress != null) progress.processStepCompleted(); } // if call was successful put appropriate status in preferences servicePreferences.setLastSyncOutStatus(ServicePreferences.SYNC_STATUS_SUCCESS); servicePreferences.setLastSyncOutFeedsCount(model.getGuidesSet().countFeeds()); } catch (ServerServiceException e1) { // synchronization errored out for some reason servicePreferences.setLastSyncOutStatus(ServicePreferences.SYNC_STATUS_FAILURE); // report only if servervice exception was caused by another error if (e1.getCause() != null) { LOG.log(Level.SEVERE, Strings.error("sync.error.during.sync.out"), e1); stats.registerFailure(null); } else { stats.registerFailure(e1.getMessage()); } } servicePreferences.setLastSyncOutDate(new Date()); return stats; } /** * Saves preferences to the service. * * @param aEmail account email. * @param aPassword account password. * @param aStats stats to fill. * * @throws ServerServiceException in case of service error. */ private void storePreferences(String aEmail, String aPassword, SyncOutStats aStats) throws ServerServiceException { final Hashtable<String, Object> prefs = new Hashtable<String, Object>(); // image blocker expressions String expressions = StringUtils.join(ImageBlocker.getExpressions().iterator(), "\n"); prefs.put(ImageBlocker.KEY, StringUtils.toUTF8(expressions)); SentimentsConfig.syncOut(prefs); storeGeneralPreferences(prefs); storeGuidesPreferences(prefs); storeFeedsPreferences(prefs); storeArticlesPreferences(prefs); storeTagsPreferences(prefs); storeReadingListsPreferences(prefs); storeAdvancedPreferences(prefs); storeWhatsHotPreferences(prefs); storeTwitterPreferences(prefs); Manager.storeState(prefs); // Save current time to record on the service setLong(prefs, "timestamp", System.currentTimeMillis()); ServerService.syncStorePrefs(aEmail, aPassword, prefs); // record number of saved preferences aStats.savedPreferences = prefs.size(); } /** * Saves guides and feeds to the service. * * @param aEmail account email. * @param aPassword account password. * @param aStats stats to fill. * * @throws ServerServiceException in case of service error. */ private void storeFeeds(String aEmail, String aPassword, SyncOutStats aStats) throws ServerServiceException { // export information about guides GuidesSet guidesSet = model.getGuidesSet(); // Calculate feed hashes basing on their present XML URLs for later updates upon successful completion Map<DirectFeed, Integer> feedHashes = calculateFeedHashes(guidesSet); OPMLGuideSet opmlSet = Converter.convertToOPML(guidesSet, "BlogBridge Feeds"); Document doc = new Exporter(true).export(opmlSet); // prepare parameters for server call String opml = Transformation.documentToString(doc); int userId = ServerService.syncStore(aEmail, aPassword, opml); updatePublishedListsURLs(guidesSet, userId); // Update synchronization times guidesSet.onSyncOutCompletion(); // Update feed with hashes updateFeedsWithHashes(feedHashes); // Remove all keys of deleted feeds as we just transfered our feeds list to the service GlobalController.SINGLETON.getDeletedFeedsRepository().purge(); // Count saved guides/feeds StandardGuide[] guides = guidesSet.getStandardGuides(null); aStats.savedGuides = guides.length; aStats.savedFeeds = countFeeds(guides); } /** * Updates feeds with URL hashes. * * @param hashes hashes. */ static void updateFeedsWithHashes(Map<DirectFeed, Integer> hashes) { for (Map.Entry<DirectFeed, Integer> en : hashes.entrySet()) { DirectFeed feed = en.getKey(); int hash = en.getValue(); feed.setSyncHash(hash); } } /** * Calculates hashes for all direct feeds in the set. The hash is calculated from the * present XML URL. * * @param set set to parse. * * @return hashes. */ static Map<DirectFeed, Integer> calculateFeedHashes(GuidesSet set) { Map<DirectFeed, Integer> hashes = new IdentityHashMap<DirectFeed, Integer>(); List<IFeed> feeds = set.getFeeds(); for (IFeed feed : feeds) { if (feed instanceof DirectFeed) { DirectFeed dfeed = (DirectFeed)feed; int hash = dfeed.calcSyncHash(); hashes.put(dfeed, hash); } } return hashes; } /** * Updates URLs of all published guides. * * @param set guides set to update. * @param userId user ID. */ private void updatePublishedListsURLs(GuidesSet set, int userId) { long publishingTime = System.currentTimeMillis(); int count = set.getGuidesCount(); for (int i = 0; i < count; i++) { IGuide guide = set.getGuideAt(i); String publishingTitle = guide.getPublishingTitle(); if (guide.isPublishingEnabled() && StringUtils.isNotEmpty(publishingTitle)) { String url = RL_PUB_URL.format(new Object[] { userId, StringUtils.encodeForURL(publishingTitle) }); guide.setPublishingURL(url); guide.setLastPublishingTime(publishingTime); } } } /** * Returns the message to be reported on synchronization start. * * @return message. */ protected String getProcessStartMessage() { return prepareProcessStartMessage( Strings.message("service.sync.message.synchronizing"), Strings.message("service.sync.message.preferences"), Strings.message("service.sync.message.guides.and.feeds"), Strings.message("service.sync.message.with.blogbridge.service") ); } /** * Returns number of feeds in guides total. * * @param guides guides list. * * @return total number of feeds. */ private static int countFeeds(IGuide[] guides) { int cnt = 0; for (IGuide guide : guides) cnt += guide.getFeedsCount(); return cnt; } /** * Simple statistics holder. */ public static class SyncOutStats extends Stats { private int savedGuides = -1; private int savedFeeds = -1; private int savedPreferences = -1; /** * Returns custom text to be told if not failed. * * @return text. */ protected String getCustomText() { StringBuffer buf = new StringBuffer(); if (savedGuides > 0) buf.append(MessageFormat.format( Strings.message("service.sync.out.status.guides.saved"), savedGuides)); if (savedFeeds > 0) buf.append(MessageFormat.format( Strings.message("service.sync.out.status.feeds.saved"), savedFeeds)); if (savedPreferences > 0) buf.append(MessageFormat.format( Strings.message("service.sync.out.status.preference.saved"), savedPreferences)); return buf.toString(); } } // --------------------------------------------------------------------------------------------- // Preferences storing // --------------------------------------------------------------------------------------------- /** * Stores general preferences. * * @param prefs preferences map. */ private void storeGeneralPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); setBoolean(prefs, UserPreferences.PROP_CHECKING_FOR_UPDATES_ON_STARTUP, up.isCheckingForUpdatesOnStartup()); // Disabled as we don't like what happens when synchronizing fonts across platforms // setFont(prefs, RenderingSettingsNames.MAIN_CONTENT_FONT, frs.getMainContentFont()); setBoolean(prefs, UserPreferences.PROP_SHOW_TOOLBAR, up.isShowToolbar()); // Behaviour setBoolean(prefs, UserPreferences.PROP_MARK_READ_WHEN_CHANGING_CHANNELS, up.isMarkReadWhenChangingChannels()); setBoolean(prefs, UserPreferences.PROP_MARK_READ_WHEN_CHANGING_GUIDES, up.isMarkReadWhenChangingGuides()); setBoolean(prefs, UserPreferences.PROP_MARK_READ_AFTER_DELAY, up.isMarkReadAfterDelay()); setInt(prefs, UserPreferences.PROP_MARK_READ_AFTER_SECONDS, up.getMarkReadAfterSeconds()); // Updates and Cleanups setInt(prefs, UserPreferences.PROP_RSS_POLL_MIN, up.getRssPollInterval()); setInt(prefs, UserPreferences.PROP_PURGE_COUNT, up.getPurgeCount()); setBoolean(prefs, UserPreferences.PROP_PRESERVE_UNREAD, up.isPreserveUnread()); } /** * Stores guides preferences. * * @param prefs preferences map. */ private void storeGuidesPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); FeedRenderingSettings frs = model.getGlobalRenderingSettings(); setBoolean(prefs, UserPreferences.PROP_PING_ON_RL_PUBLICATION, up.isPingOnReadingListPublication()); setString(prefs, UserPreferences.PROP_PING_ON_RL_PUBLICATION_URL, up.getPingOnReadingListPublicationURL()); setBoolean(prefs, RenderingSettingsNames.IS_BIG_ICON_IN_GUIDES, frs.isBigIconInGuides()); setBoolean(prefs, "showUnreadInGuides", frs.isShowUnreadInGuides()); setBoolean(prefs, RenderingSettingsNames.IS_ICON_IN_GUIDES_SHOWING, frs.isShowIconInGuides()); setBoolean(prefs, RenderingSettingsNames.IS_TEXT_IN_GUIDES_SHOWING, frs.isShowTextInGuides()); setInt(prefs, UserPreferences.PROP_GUIDE_SELECTION_MODE, up.getGuideSelectionMode()); } /** * Stores feeds preferences. * * @param prefs preferences map. */ private void storeFeedsPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); FeedRenderingSettings frs = model.getGlobalRenderingSettings(); setBoolean(prefs, "showStarz", frs.isShowStarz()); setBoolean(prefs, "showUnreadInFeeds", frs.isShowUnreadInFeeds()); setBoolean(prefs, "showActivityChart", frs.isShowActivityChart()); setFilterColor(prefs, FeedClass.DISABLED); setFilterColor(prefs, FeedClass.INVALID); setFilterColor(prefs, FeedClass.LOW_RATED); setFilterColor(prefs, FeedClass.READ); setFilterColor(prefs, FeedClass.UNDISCOVERED); setBoolean(prefs, UserPreferences.PROP_SORTING_ENABLED, up.isSortingEnabled()); setInt(prefs, UserPreferences.PROP_SORT_BY_CLASS_1, up.getSortByClass1()); setInt(prefs, UserPreferences.PROP_SORT_BY_CLASS_2, up.getSortByClass2()); setBoolean(prefs, UserPreferences.PROP_REVERSED_SORT_BY_CLASS_1, up.isReversedSortByClass1()); setBoolean(prefs, UserPreferences.PROP_REVERSED_SORT_BY_CLASS_2, up.isReversedSortByClass2()); } /** * Stores articles preferences. * * @param prefs preferences map. */ private void storeArticlesPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); FeedRenderingSettings frs = model.getGlobalRenderingSettings(); setBoolean(prefs, "groupingEnabled", frs.isGroupingEnabled()); setBoolean(prefs, "suppressingOlderThan", frs.isSuppressingOlderThan()); setBoolean(prefs, "displayingFullTitles", frs.isDisplayingFullTitles()); setBoolean(prefs, "sortingAscending", frs.isSortingAscending()); setInt(prefs, "suppressOlderThan", frs.getSuppressOlderThan()); setBoolean(prefs, UserPreferences.PROP_COPY_LINKS_IN_HREF_FORMAT, up.isCopyLinksInHrefFormat()); setBoolean(prefs, "showEmptyGroups", frs.isShowEmptyGroups()); setBoolean(prefs, UserPreferences.PROP_BROWSE_ON_DBL_CLICK, up.isBrowseOnDblClick()); setBoolean(prefs, UserPreferences.PROP_AUTO_EXPAND_MINI, up.isAutoExpandMini()); up.getViewModePreferences().store(prefs); } /** * Stores tags preferences. * * @param prefs preferences map. */ private void storeTagsPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); setInt(prefs, UserPreferences.PROP_TAGS_STORAGE, up.getTagsStorage()); setString(prefs, UserPreferences.PROP_TAGS_DELICIOUS_USER, up.getTagsDeliciousUser()); setString(prefs, UserPreferences.PROP_TAGS_DELICIOUS_PASSWORD, up.getTagsDeliciousPassword()); setBoolean(prefs, UserPreferences.PROP_TAGS_AUTOFETCH, up.isTagsAutoFetch()); setBoolean(prefs, UserPreferences.PROP_PIN_TAGGING, up.isPinTagging()); setString(prefs, UserPreferences.PROP_PIN_TAGS, up.getPinTags()); } /** * Stores reading lists preferences. * * @param prefs preferences map. */ private void storeReadingListsPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); setLong(prefs, UserPreferences.PROP_READING_LIST_UPDATE_PERIOD, up.getReadingListUpdatePeriod()); setInt(prefs, UserPreferences.PROP_ON_READING_LIST_UPDATE_ACTIONS, up.getOnReadingListUpdateActions()); setBoolean(prefs, UserPreferences.PROP_UPDATE_FEEDS, up.isUpdateFeeds()); setBoolean(prefs, UserPreferences.PROP_UPDATE_READING_LISTS, up.isUpdateReadingLists()); } /** * Stores advanced preferences. * * @param prefs preferences map. */ private void storeAdvancedPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); StarzPreferences sp = model.getStarzPreferences(); setInt(prefs, UserPreferences.PROP_FEED_SELECTION_DELAY, up.getFeedSelectionDelay()); setBoolean(prefs, UserPreferences.PROP_AA_TEXT, up.isAntiAliasText()); setInt(prefs, StarzPreferences.PROP_TOP_ACTIVITY, sp.getTopActivity()); setInt(prefs, StarzPreferences.PROP_TOP_HIGHLIGHTS, sp.getTopHighlights()); setBoolean(prefs, UserPreferences.PROP_SHOW_TOOLBAR_LABELS, up.isShowToolbarLabels()); setBoolean(prefs, UserPreferences.PROP_SHOW_UNREAD_BUTTON_MENU, up.isShowUnreadButtonMenu()); setInt(prefs, UserPreferences.PROP_FEED_IMPORT_LIMIT, up.getFeedImportLimit()); } /** * Stores what's hot preferences. * * @param prefs preferences map. */ private void storeWhatsHotPreferences(Map prefs) { UserPreferences up = model.getUserPreferences(); setString(prefs, UserPreferences.PROP_WH_IGNORE, up.getWhIgnore()); setBoolean(prefs, UserPreferences.PROP_WH_NOSELFLINKS, up.isWhNoSelfLinks()); setBoolean(prefs, UserPreferences.PROP_WH_SUPPRESS_SAME_SOURCE_LINKS, up.isWhSuppressSameSourceLinks()); setString(prefs, UserPreferences.PROP_WH_TARGET_GUIDE, up.getWhTargetGuide()); setLong(prefs, UserPreferences.PROP_WH_SETTINGS_CHANGE_TIME, up.getWhSettingsChangeTime()); } /** * Stores Twitter preferences. * * @param prefs prefs. */ private void storeTwitterPreferences(Map prefs) { TwitterPreferences tp = model.getUserPreferences().getTwitterPreferences(); setBoolean(prefs, TwitterPreferences.PROP_TWITTER_ENABLED, tp.isEnabled()); setString(prefs, TwitterPreferences.PROP_TWITTER_SCREEN_NAME, tp.getScreenName()); setString(prefs, TwitterPreferences.PROP_TWITTER_ACCESS_TOKEN, tp.getAccessToken()); setString(prefs, TwitterPreferences.PROP_TWITTER_TOKEN_SECRET, tp.getTokenSecret()); setBoolean(prefs, TwitterPreferences.PROP_TWITTER_PROFILE_PICS, tp.isProfilePics()); setBoolean(prefs, TwitterPreferences.PROP_TWITTER_PASTE_LINK, tp.isPasteLink()); } /** * Saves boolean to preferences map. * * @param prefs preferences map. * @param name property name. * @param value value. */ public static void setBoolean(Map prefs, String name, boolean value) { setString(prefs, name, Boolean.toString(value)); } /** * Saves integer property to preferences map. * * @param prefs preferences map. * @param name property name. * @param value value. */ public static void setInt(Map prefs, String name, int value) { setString(prefs, name, Integer.toString(value)); } private static void setLong(Map prefs, String name, long value) { setString(prefs, name, Long.toString(value)); } private static void setFilterColor(Map prefs, int feedClass) { FeedDisplayModeManager fdmm = FeedDisplayModeManager.getInstance(); Color color = fdmm.getColor(feedClass); setString(prefs, "cdmm." + feedClass, UifUtilities.colorToHex(color)); } public static void setString(Map prefs, String name, String value) { byte[] bytes = StringUtils.toUTF8(value); prefs.put(name, bytes == null ? new byte[0] : bytes); } // ----------------------------------------------------------------------------------------------------------------- // Pinging // ----------------------------------------------------------------------------------------------------------------- /** * Ping URL with all published reading lists. */ private void pingGuides() { Thread thPing = new Thread(new Runnable() { public void run() { // Ping URL GlobalModel model = GlobalController.SINGLETON.getModel(); UserPreferences prefs = model.getUserPreferences(); String url = prefs.getPingOnReadingListPublicationURL().trim(); if (prefs.isPingOnReadingListPublication() && url.length() > 0 && url.indexOf("%u") != -1) { IGuide[] publishedGuides = collectGuides(model.getGuidesSet()); pingGuides(publishedGuides, url); } } }, THREAD_NAME_PING); thPing.start(); } /** * Pings all guides one by one. * * @param guides guide list. * @param url URL to ping with guide publication URL included. */ private void pingGuides(IGuide[] guides, String url) { for (IGuide guide : guides) { String realURL = url.replaceAll("%u", guide.getPublishingURL()); try { ping(new URL(realURL)); } catch (Throwable e) { LOG.log(Level.WARNING, Strings.error("sync.failed.to.ping.reading.list.service"), e); } } } /** * Pings the URL. * * @param url url to ping. * * @throws java.io.IOException I/O exception. */ private void ping(URL url) throws IOException { // Read one byte to make sure that we connected InputStream stream = url.openStream(); //noinspection ResultOfMethodCallIgnored stream.read(); stream.close(); } /** * Returns all guides which have publication flag set and the publication URL available. * * @param set guides set. * * @return list of guides. */ private IGuide[] collectGuides(GuidesSet set) { StandardGuide[] guides = set.getStandardGuides(null); java.util.List<StandardGuide> rl = new ArrayList<StandardGuide>(); for (StandardGuide guide : guides) { String url = guide.getPublishingURL(); if (guide.isPublishingEnabled() && url != null && url.trim().length() > 0) rl.add(guide); } return rl.toArray(new IGuide[rl.size()]); } }