// 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: HsqlFeedsPM.java,v 1.52 2008/02/28 15:59:51 spyromus Exp $ // package com.salas.bb.persistence.backend; import com.salas.bb.domain.*; import com.salas.bb.domain.querytypes.QueryType; import com.salas.bb.persistence.PersistenceException; import com.salas.bb.utils.StringUtils; import com.salas.bb.utils.i18n.Strings; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; import java.text.MessageFormat; import java.util.logging.Level; import java.util.logging.Logger; /** * HSQL feeds persistence manager. */ final class HsqlFeedsPM { private static final Logger LOG = Logger.getLogger(HsqlFeedsPM.class.getName()); /** Persistence manager context. */ private final HsqlPersistenceManager context; /** Feed is not in database yet. */ private static final String MSG_NOT_IN_DB = Strings.error("db.feed.is.not.in.database"); /** Feed is already in database. */ private static final String MSG_ALREADY_IN_DB = Strings.error("db.feed.is.already.in.database"); /** Feed is of unsupported type. */ private static final String MSG_UNSUPPORTED_TYPE = Strings.error("db.feed.is.of.unsupported.type"); /** * Creates manager. * * @param aContext context to communicate with. */ public HsqlFeedsPM(HsqlPersistenceManager aContext) { context = aContext; } /** * Inserts the feed into database. * * @param feed feed to insert. * * @throws NullPointerException if feed isn't specified. * @throws IllegalStateException if feed is already in database. * @throws IllegalArgumentException if feed is of unsupported type. * @throws SQLException if database operation fails. */ public void insertFeed(IFeed feed) throws SQLException { if (feed == null) throw new NullPointerException(HsqlPersistenceManager.MSG_FEED_UNSPECIFIED); if (feed.getID() != -1) throw new IllegalStateException(MSG_ALREADY_IN_DB); if (!hasSupportedType(feed)) throw new IllegalArgumentException(MSG_UNSUPPORTED_TYPE); // Insert general Feed record feed.setID(insertGeneralFeed(feed)); // Insert a stats record long now = System.currentTimeMillis(); PreparedStatement stmt = context.getPreparedStatement("INSERT INTO FEEDSTATS " + "(FEEDID, INIT_TIME, RESET_TIME) VALUES (?, ?, ?)"); stmt.setLong(1, feed.getID()); stmt.setLong(2, now); stmt.setLong(3, now); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException("Failed to add a feed stats row"); } /** * Returns <code>TRUE</code> if the feed has supported type. * * @param feed feed. * * @return <code>TRUE</code> if the feed has supported type. */ private static boolean hasSupportedType(IFeed feed) { return (feed instanceof DirectFeed) || (feed instanceof QueryFeed) || (feed instanceof SearchFeed); } /** * Inserts record into DirectFeeds table associated with given Feeds table ID. * * @param feedId ID from Feeds table. * @param directFeed direct feed to take data from. * * @throws SQLException if database fails. */ private void insertDirectFeed(long feedId, DirectFeed directFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("INSERT INTO DIRECTFEEDS " + "(FEEDID, TITLE, AUTHOR, DESCRIPTION, CUSTOMTITLE, CUSTOMAUTHOR, " + "CUSTOMDESCRIPTION, DEAD, SITEURL, XMLURL, INLINKS, LASTMETADATAUPDATETIME, " + "USERTAGS, UNSAVEDUSERTAGS, TAGSDESCRIPTION, TAGSEXTENDED, DISABLED, SYNC_HASH) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); try { stmt.setLong(1, feedId); stmt.setString(2, directFeed.getBaseTitle()); stmt.setString(3, directFeed.getBaseAuthor()); stmt.setString(4, directFeed.getBaseDescription()); stmt.setString(5, directFeed.getCustomTitle()); stmt.setString(6, directFeed.getCustomAuthor()); stmt.setString(7, directFeed.getCustomDescription()); stmt.setBoolean(8, directFeed.isDead()); URL siteURL = directFeed.getSiteURL(); stmt.setString(9, siteURL == null ? null : siteURL.toString()); URL xmlURL = directFeed.getXmlURL(); stmt.setString(10, xmlURL == null ? null : xmlURL.toString()); stmt.setInt(11, directFeed.getInLinks()); stmt.setLong(12, directFeed.getLastMetaDataUpdateTime()); stmt.setString(13, StringUtils.arrayToQuotedKeywords(directFeed.getUserTags())); stmt.setBoolean(14, directFeed.hasUnsavedUserTags()); stmt.setString(15, directFeed.getTagsDescription()); stmt.setString(16, directFeed.getTagsExtended()); stmt.setBoolean(17, directFeed.isDisabled()); stmt.setInt(18, directFeed.getSyncHash()); // ReadingList list = directFeed.getReadingList(); // stmt.setObject(18, list == null ? null : new Integer((int)list.getID())); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException( Strings.error("db.failed.to.insert.row.into.directfeeds")); } finally { stmt.close(); } } /** * Inserts records into SearchFeed table associated with given Feeds table ID. * * @param aFeedId ID from Feeds table. * @param aSearchFeed data feed. * * @throws SQLException if database fails. */ private void insertSearchFeed(long aFeedId, SearchFeed aSearchFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("INSERT INTO SEARCHFEEDS " + "(FEEDID, ARTICLESLIMIT, RATING, TITLE, QUERY, DEDUP_ENABLED, DEDUP_FROM, DEDUP_TO) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); try { stmt.setLong(1, aFeedId); stmt.setInt(2, aSearchFeed.getArticlesLimit()); stmt.setInt(3, aSearchFeed.getRating()); stmt.setString(4, aSearchFeed.getBaseTitle()); stmt.setString(5, aSearchFeed.getQuery().serializeToString()); stmt.setBoolean(6, aSearchFeed.isDedupEnabled()); stmt.setInt(7, aSearchFeed.getDedupFrom()); stmt.setInt(8, aSearchFeed.getDedupTo()); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(Strings.error("db.failed.to.insert.row.into.searchfeeds")); } finally { stmt.close(); } } /** * Inserts records into DataFeeds table associated with given Feeds table ID. * * @param feedId ID from Feeds table. * @param dataFeed data feed. * * @throws SQLException if database fails. */ private void insertDataFeed(long feedId, DataFeed dataFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("INSERT INTO DATAFEEDS " + "(FEEDID, INITTIME, LASTPOLLTIME, RETRIEVALS, FORMAT, LANGUAGE, PURGELIMIT, " + "UPDATEPERIOD, TOTALPOLLEDARTICLES, " + "RATING, LASTUPDATESERVERTIME, LASTFETCHARTICLEKEYS) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); try { stmt.setLong(1, feedId); stmt.setLong(2, dataFeed.getInitTime()); stmt.setLong(3, dataFeed.getLastPollTime()); stmt.setInt(4, dataFeed.getRetrievals()); stmt.setString(5, dataFeed.getFormat()); stmt.setString(6, dataFeed.getLanguage()); stmt.setInt(7, dataFeed.getPurgeLimit()); stmt.setLong(8, dataFeed.getUpdatePeriod()); stmt.setInt(9, dataFeed.getTotalPolledArticles()); stmt.setInt(10, dataFeed.getRating()); stmt.setLong(11, dataFeed.getLastUpdateServerTime()); String[] keys = dataFeed.getLastFetchArticleKeys(); stmt.setString(12, keys == null ? null : StringUtils.join(keys, ",")); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(Strings.error("db.failed.to.insert.row.into.datafeeds")); if (dataFeed instanceof DirectFeed) insertDirectFeed(feedId, (DirectFeed)dataFeed); else if (dataFeed instanceof QueryFeed) insertQueryFeed(feedId, (QueryFeed)dataFeed); } finally { stmt.close(); } } /** * Inserts record into QueryFeeds table associated with given DataFeeds table ID. * * @param feedId ID from Feeds table. * @param queryFeed query feed to take data from. * * @throws SQLException if database fails. */ private void insertQueryFeed(long feedId, QueryFeed queryFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("INSERT INTO QUERYFEEDS " + "(FEEDID, TITLE, QUERYTYPE, KEYWORDS, DEDUP_ENABLED, DEDUP_FROM, DEDUP_TO) " + "VALUES (?, ?, ?, ?, ?, ? ,?)"); try { QueryType queryType = queryFeed.getQueryType(); int type = queryType == null ? -1 : queryType.getType(); stmt.setLong(1, feedId); stmt.setString(2, queryFeed.getBaseTitle()); stmt.setInt(3, type); stmt.setString(4, queryFeed.getParameter()); stmt.setBoolean(5, queryFeed.isDedupEnabled()); stmt.setInt(6, queryFeed.getDedupFrom()); stmt.setInt(7, queryFeed.getDedupTo()); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(Strings.error("db.failed.to.insert.row.into.queryfeeds")); } finally { stmt.close(); } } /** * Inserts record into Feeds table for a feed of a given type. * * @param feed feed to insert. * * @return ID of inserted feed. * * @throws SQLException if database fails. */ private long insertGeneralFeed(IFeed feed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement( "INSERT INTO FEEDS (INVALIDNESSREASON, TYPE, FEEDTYPE) VALUES (?, ?, ?)"); // Find out the type of the feed. int type; if (feed instanceof DirectFeed) { type = 0; } else if (feed instanceof QueryFeed) { type = 1; } else if (feed instanceof SearchFeed) { type = 2; } else throw new IllegalArgumentException(MSG_UNSUPPORTED_TYPE); try { stmt.setString(1, feed.getInvalidnessReason()); stmt.setInt(2, type); // stmt.setLong(3, feed.getGuide().getID()); stmt.setInt(3, feed.getType().getType()); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(Strings.error("db.row.was.not.added.for.new.feed")); } finally { stmt.close(); } long id = context.getInsertedID(); insertGeneralFeedProperties(id, feed); if (feed instanceof DataFeed) { insertDataFeed(id, (DataFeed)feed); } else { insertSearchFeed(id, (SearchFeed)feed); } return id; } /** * Inserts record with properties. * * @param aFeedId feed ID. * @param aFeed feed. * * @throws SQLException if database fails. */ private void insertGeneralFeedProperties(long aFeedId, IFeed aFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement( "INSERT INTO FEEDSPROPERTIES (FEEDID, LASTVISITTIME, " + "CUSTOMVIEWMODEENABLED, CUSTOMVIEWMODE, VIEWS, CLICKTHROUGHS, " + "ASCENDINGSORTING, ASA, ASA_FOLDER, ASA_NAMEFORMAT, ASE, ASE_FOLDER, ASE_NAMEFORMAT, " + "HANDLING_TYPE) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); try { stmt.setLong(1, aFeedId); stmt.setLong(2, aFeed.getLastVisitTime()); stmt.setBoolean(3, aFeed.isCustomViewModeEnabled()); stmt.setInt(4, aFeed.getCustomViewMode()); stmt.setInt(5, aFeed.getViews()); stmt.setInt(6, aFeed.getClickthroughs()); stmt.setObject(7, aFeed.getAscendingSorting()); stmt.setBoolean(8, aFeed.isAutoSaveArticles()); stmt.setString(9, aFeed.getAutoSaveArticlesFolder()); stmt.setString(10, aFeed.getAutoSaveArticlesNameFormat()); stmt.setBoolean(11, aFeed.isAutoSaveEnclosures()); stmt.setString(12, aFeed.getAutoSaveEnclosuresFolder()); stmt.setString(13, aFeed.getAutoSaveEnclosuresNameFormat()); FeedHandlingType handlingType = aFeed.getHandlingType(); stmt.setInt(14, handlingType == null ? FeedHandlingType.DEFAULT.toInteger() : handlingType.toInteger()); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(Strings.error("db.row.was.not.added.at.feedsproperties")); } finally { stmt.close(); } } /** * Removes the feed from database. * * @param feed feed to remove. * * @throws NullPointerException if feed isn't specified. * @throws IllegalStateException if feed is not in database. * @throws SQLException if database operation fails. */ public void removeFeed(IFeed feed) throws SQLException { if (feed == null) throw new NullPointerException(HsqlPersistenceManager.MSG_FEED_UNSPECIFIED); if (feed.getID() == -1) throw new IllegalStateException(MSG_NOT_IN_DB); if (!hasSupportedType(feed)) throw new IllegalArgumentException(MSG_UNSUPPORTED_TYPE); PreparedStatement stmt = context.getPreparedStatement("DELETE FROM FEEDS WHERE ID=?"); stmt.setLong(1, feed.getID()); try { int rows = stmt.executeUpdate(); if (rows == 0) { LOG.log(Level.SEVERE, MessageFormat.format( Strings.error("db.no.rows.deleted.from.feeds.feedid.0"), feed.getID())); } HsqlPersistenceManager.clearFeedID(feed); } finally { stmt.close(); } } /** * Moves feed from dest guide to destination guide. * * @param feed feed to move. * @param source destination guide. * @param dest dest guide. * * @throws NullPointerException if feed or destination guides aren't specified. * @throws IllegalStateException if feed or guide are transient. * @throws SQLException if database operation fails. */ public void moveFeed(IFeed feed, IGuide source, IGuide dest) throws SQLException, IllegalStateException { if (feed == null) throw new NullPointerException(HsqlPersistenceManager.MSG_FEED_UNSPECIFIED); if (source == null) throw new NullPointerException(Strings.error("unspecified.source.guide")); if (dest == null) throw new NullPointerException(Strings.error("unspecified.destination.guide")); long sourceGuideId = source.getID(); long destGuideId = dest.getID(); long feedId = feed.getID(); if (feedId == -1) throw new IllegalStateException(Strings.error("db.feed.is.transient")); if (sourceGuideId == -1) throw new IllegalStateException(Strings.error("db.source.guide.is.transient")); if (destGuideId == -1) throw new IllegalStateException(Strings.error("db.destination.guide.is.transient")); PreparedStatement updFeed = context.getPreparedStatement( "UPDATE FEEDS2GUIDES SET GUIDEID=?, POSITN=? WHERE FEEDID=? AND GUIDEID=?"); try { updFeed.setLong(1, destGuideId); updFeed.setInt(2, dest.indexOf(feed)); updFeed.setLong(3, feedId); updFeed.setLong(4, sourceGuideId); int rows = updFeed.executeUpdate(); if (rows == 0) { LOG.log(Level.SEVERE, MessageFormat.format( Strings.error("db.failed.to.move.feed.feedid.0.sourceguideid.1.destguideid.2"), feedId, sourceGuideId, destGuideId)); } } finally { updFeed.close(); } } /** * Updates the feed in database. * * @param feed feed to update. * @param property name of property being updated or NULL if full update required. * * @throws NullPointerException if feed isn't specified. * @throws IllegalStateException if feed is not in database. * @throws SQLException if database operation fails. */ public void updateFeed(IFeed feed, String property) throws SQLException { if (feed == null) throw new NullPointerException(HsqlPersistenceManager.MSG_FEED_UNSPECIFIED); if (feed.getID() == -1) throw new IllegalStateException(MSG_NOT_IN_DB); if (!hasSupportedType(feed)) throw new IllegalArgumentException(MSG_UNSUPPORTED_TYPE); if (!updateFeedsTable(feed, property)) { if (feed instanceof DataFeed) { updateDataFeedsTable((DataFeed)feed); if (feed instanceof DirectFeed) { updateDirectFeedsTable((DirectFeed)feed); } else if (feed instanceof QueryFeed) { updateQueryFeedsTable((QueryFeed)feed); } } else if (feed instanceof SearchFeed) { updateSearchFeedsTable((SearchFeed)feed, property); } } } /** * Updates the SearchFeeds table with data taken from feed object. * * @param aFeed feed to take data from. * @param property name of property being updated or NULL if full update required. * * @throws SQLException if database error happens. */ private void updateSearchFeedsTable(SearchFeed aFeed, String property) throws SQLException { PreparedStatement stmt = null; try { if (SearchFeed.PROP_QUERY.equals(property)) { stmt = context.getPreparedStatement("UPDATE SEARCHFEEDS SET " + "QUERY=? WHERE FEEDID=?"); stmt.setString(1, aFeed.getQuery().serializeToString()); stmt.setLong(2, aFeed.getID()); } else { stmt = context.getPreparedStatement("UPDATE SEARCHFEEDS SET " + "TITLE=?, ARTICLESLIMIT=?, RATING=?, DEDUP_ENABLED=?, DEDUP_FROM=?, DEDUP_TO=? " + "WHERE FEEDID=?"); stmt.setString(1, aFeed.getBaseTitle()); stmt.setInt(2, aFeed.getArticlesLimit()); stmt.setInt(3, aFeed.getRating()); stmt.setBoolean(4, aFeed.isDedupEnabled()); stmt.setInt(5, aFeed.getDedupFrom()); stmt.setInt(6, aFeed.getDedupTo()); stmt.setLong(7, aFeed.getID()); } int rows = stmt.executeUpdate(); if (rows == 0) logNoUpdate(aFeed, "SEARCHFEEDS"); } finally { if (stmt != null) stmt.close(); } } /** * Updates the QueryFeeds table with data taken from feed object. * * @param feed feed to take data from. * * @throws SQLException if database error happens. */ private void updateQueryFeedsTable(QueryFeed feed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("UPDATE QUERYFEEDS SET " + "TITLE=?, QUERYTYPE=?, KEYWORDS=?, DEDUP_ENABLED=?, DEDUP_FROM=?, DEDUP_TO=? " + "WHERE FEEDID=?"); try { QueryType queryType = feed.getQueryType(); int type = queryType == null ? -1 : queryType.getType(); stmt.setString(1, feed.getBaseTitle()); stmt.setInt(2, type); stmt.setString(3, feed.getParameter()); stmt.setBoolean(4, feed.isDedupEnabled()); stmt.setInt(5, feed.getDedupFrom()); stmt.setInt(6, feed.getDedupTo()); stmt.setLong(7, feed.getID()); int rows = stmt.executeUpdate(); if (rows == 0) logNoUpdate(feed, "QUERYFEEDS"); } finally { stmt.close(); } } /** * Updates the Feeds table with data taken from feed object. * * @param feed feed to take data from. * @param property property which has been updated in the feed or NULL if doesn't matter. * * @return <code>TRUE</code> if updated. * * @throws SQLException if database error happens. */ private boolean updateFeedsTable(IFeed feed, String property) throws SQLException { PreparedStatement stmt = null; boolean updated = false; try { // If it was a property which isn't stored in this table then skip update if (property == null || (updated = (property.equals(IFeed.PROP_INVALIDNESS_REASON) || property.equals(IFeed.PROP_TYPE)))) { stmt = context.getPreparedStatement("UPDATE FEEDS SET " + "INVALIDNESSREASON=?, FEEDTYPE=? WHERE ID=?"); stmt.setString(1, feed.getInvalidnessReason()); stmt.setInt(2, feed.getType().getType()); stmt.setLong(3, feed.getID()); int rows = stmt.executeUpdate(); if (rows == 0) logNoUpdate(feed, "FEEDS"); } } finally { if (stmt != null) stmt.close(); } try { if (!updated && (property == null || property.equals(IFeed.PROP_LAST_VISIT_TIME) || property.equals(IFeed.PROP_CUSTOM_VIEW_MODE_ENABLED) || property.equals(IFeed.PROP_CUSTOM_VIEW_MODE) || property.equals(IFeed.PROP_LAST_UPDATE_TIME) || property.equals(IFeed.PROP_VIEWS) || property.equals(IFeed.PROP_CLICKTHROUGHS) || property.equals(IFeed.PROP_ASCENDING_SORTING) || property.startsWith(IFeed.PROP_AUTO_SAVE_ARTICLES) || property.startsWith(IFeed.PROP_AUTO_SAVE_ENCLOSURES))) { // if it wasn't specific property we should tell FALSE to // make the other (following) updates updated = property != null; stmt = context.getPreparedStatement( "UPDATE FEEDSPROPERTIES SET " + "LASTVISITTIME=?, CUSTOMVIEWMODEENABLED=?," + "CUSTOMVIEWMODE=?, LASTUPDATETIME=?, VIEWS=?, CLICKTHROUGHS=?, " + "ASCENDINGSORTING=?, ASA=?, ASA_FOLDER=?, ASA_NAMEFORMAT=?, " + "ASE=?, ASE_FOLDER=?, ASE_NAMEFORMAT=?, HANDLING_TYPE=? WHERE FEEDID=?"); stmt.setLong(1, feed.getLastVisitTime()); stmt.setBoolean(2, feed.isCustomViewModeEnabled()); stmt.setInt(3, feed.getCustomViewMode()); stmt.setLong(4, feed.getLastUpdateTime()); stmt.setInt(5, feed.getViews()); stmt.setInt(6, feed.getClickthroughs()); stmt.setObject(7, feed.getAscendingSorting()); stmt.setBoolean(8, feed.isAutoSaveArticles()); stmt.setString(9, feed.getAutoSaveArticlesFolder()); stmt.setString(10, feed.getAutoSaveArticlesNameFormat()); stmt.setBoolean(11, feed.isAutoSaveEnclosures()); stmt.setString(12, feed.getAutoSaveEnclosuresFolder()); stmt.setString(13, feed.getAutoSaveEnclosuresNameFormat()); FeedHandlingType handlingType = feed.getHandlingType(); stmt.setInt(14, handlingType == null ? FeedHandlingType.DEFAULT.toInteger() : handlingType.toInteger()); stmt.setLong(15, feed.getID()); int rows = stmt.executeUpdate(); if (rows == 0) { // Perhaps, there's no row in feeds properties table insertGeneralFeedProperties(feed.getID(), feed); } } } finally { if (stmt != null) stmt.close(); } return updated; } /** * Updates the DataFeeds table with data taken from feed object. * * @param dataFeed feed to take data from. * * @throws SQLException if database error happens. */ private void updateDataFeedsTable(DataFeed dataFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("UPDATE DATAFEEDS SET " + "INITTIME=?, LASTPOLLTIME=?, RETRIEVALS=?, FORMAT=?, LANGUAGE=?, PURGELIMIT=?, " + "UPDATEPERIOD=?, " + "TOTALPOLLEDARTICLES=?, RATING=?, LASTUPDATESERVERTIME=?, LASTFETCHARTICLEKEYS=? " + "WHERE FEEDID=?"); try { stmt.setLong(1, dataFeed.getInitTime()); stmt.setLong(2, dataFeed.getLastPollTime()); stmt.setInt(3, dataFeed.getRetrievals()); stmt.setString(4, dataFeed.getFormat()); stmt.setString(5, dataFeed.getLanguage()); stmt.setInt(6, dataFeed.getPurgeLimit()); stmt.setLong(7, dataFeed.getUpdatePeriod()); stmt.setInt(8, dataFeed.getTotalPolledArticles()); stmt.setInt(9, dataFeed.getRating()); stmt.setLong(10, dataFeed.getLastUpdateServerTime()); String[] keys = dataFeed.getLastFetchArticleKeys(); stmt.setString(11, keys == null ? null : StringUtils.join(keys, ",")); stmt.setLong(12, dataFeed.getID()); int rows = stmt.executeUpdate(); if (rows == 0) logNoUpdate(dataFeed, "DATAFEEDS"); } finally { stmt.close(); } } /** * Updates the DirectFeeds table with data taken from feed object. * * @param directFeed feed to take data from. * * @throws SQLException if database error happens. */ private void updateDirectFeedsTable(DirectFeed directFeed) throws SQLException { PreparedStatement stmt = context.getPreparedStatement("UPDATE DIRECTFEEDS SET " + "TITLE=?, AUTHOR=?, DESCRIPTION=?, CUSTOMTITLE=?, CUSTOMAUTHOR=?, " + "CUSTOMDESCRIPTION=?, DEAD=?, SITEURL=?, XMLURL=?, INLINKS=?, " + "LASTMETADATAUPDATETIME=?, USERTAGS=?, UNSAVEDUSERTAGS=?, TAGSDESCRIPTION=?, " + "TAGSEXTENDED=?, DISABLED=?, SYNC_HASH=? " + "WHERE FEEDID=?"); try { stmt.setString(1, directFeed.getBaseTitle()); stmt.setString(2, directFeed.getBaseAuthor()); stmt.setString(3, directFeed.getBaseDescription()); stmt.setString(4, directFeed.getCustomTitle()); stmt.setString(5, directFeed.getCustomAuthor()); stmt.setString(6, directFeed.getCustomDescription()); stmt.setBoolean(7, directFeed.isDead()); URL siteURL = directFeed.getSiteURL(); stmt.setString(8, siteURL == null ? null : siteURL.toString()); URL xmlURL = directFeed.getXmlURL(); stmt.setString(9, xmlURL == null ? null : xmlURL.toString()); stmt.setInt(10, directFeed.getInLinks()); stmt.setLong(11, directFeed.getLastMetaDataUpdateTime()); stmt.setString(12, StringUtils.arrayToQuotedKeywords(directFeed.getUserTags())); stmt.setBoolean(13, directFeed.hasUnsavedUserTags()); stmt.setString(14, directFeed.getTagsDescription()); stmt.setString(15, directFeed.getTagsExtended()); stmt.setBoolean(16, directFeed.isDisabled()); stmt.setInt(17, directFeed.getSyncHash()); stmt.setLong(18, directFeed.getID()); int rows = stmt.executeUpdate(); if (rows == 0) logNoUpdate(directFeed, "DIRECTFEEDS"); } finally { stmt.close(); } } /** * Puts message in log about no updates at some table. * * @param feed feed. * @param table table name. */ private static void logNoUpdate(IFeed feed, String table) { LOG.log(Level.SEVERE, MessageFormat.format( Strings.error("db.hsql.updated.0.rows.at.0.feedid.1"), table, feed.getID())); } /** * Adds a feed to a guide. * * @param aGuide guide to add feed to. * @param aFeed feed to add. * * @throws SQLException if database operation fails. * @throws IllegalStateException if guide is transient. */ public void addFeedToGuide(IGuide aGuide, IFeed aFeed) throws SQLException { if (aGuide.getID() == -1) { throw new IllegalStateException(Strings.error("db.guide.should.be.persistent")); } PreparedStatement stmt = context.getPreparedStatement( "INSERT INTO FEEDS2GUIDES (FEEDID, GUIDEID, POSITN) VALUES (?, ?, ?)"); stmt.setLong(1, aFeed.getID()); stmt.setLong(2, aGuide.getID()); stmt.setLong(3, aGuide.indexOf(aFeed)); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(MessageFormat.format( Strings.error("db.feed.was.not.linked.to.guide"), aFeed, aGuide)); } /** * Removes a feed from a guide. * * @param aGuide guide to remove feed from. * @param aFeed feed to remove. * * @throws SQLException if database operation fails. * @throws IllegalStateException if guide is transient. * @throws PersistenceException if removing feeds with no refs failed. */ public void removeFeedFromGuide(IGuide aGuide, IFeed aFeed) throws SQLException, PersistenceException { if (aGuide.getID() == -1) { throw new IllegalStateException(Strings.error("db.guide.should.be.persistent")); } PreparedStatement stmt = context.getPreparedStatement( "DELETE FROM FEEDS2GUIDES WHERE FEEDID=? AND GUIDEID=?"); stmt.setLong(1, aFeed.getID()); stmt.setLong(2, aGuide.getID()); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(MessageFormat.format( Strings.error("db.feed.was.not.linked.to.guide"), aFeed, aGuide)); context.removeFeedIfNoRefs(aFeed); } /** * Adds a feed to a reading list. * * @param aList reading list. * @param aFeed feed to add. * * @throws SQLException if database operation fails. * @throws IllegalStateException if the list is transient. */ public void addFeedToReadingList(ReadingList aList, IFeed aFeed) throws SQLException { if (aList.getID() == -1) throw new IllegalStateException(Strings.error("db.reading.list.is.transient")); PreparedStatement stmt = context.getPreparedStatement( "INSERT INTO FEEDS2READINGLISTS (FEEDID, READINGLISTID, POSITN) VALUES (?, ?, ?)"); StandardGuide guide = aList.getParentGuide(); stmt.setLong(1, aFeed.getID()); stmt.setLong(2, aList.getID()); stmt.setLong(3, guide == null ? 0 : guide.indexOf(aFeed)); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(MessageFormat.format( Strings.error("db.feed.was.not.linked.to.reading.list"), aFeed, aList)); } /** * Removes a feed from the reading list. * * @param aList list. * @param aFeed feed. * * @throws SQLException if database operation fails. * @throws IllegalStateException if the list is transient. * @throws PersistenceException if removing feeds with no refs failed. */ public void removeFeedFromReadingList(ReadingList aList, IFeed aFeed) throws SQLException, PersistenceException { if (aList.getID() == -1) throw new IllegalStateException(Strings.error("db.reading.list.is.transient")); PreparedStatement stmt = context.getPreparedStatement( "DELETE FROM FEEDS2READINGLISTS WHERE FEEDID=? AND READINGLISTID=?"); stmt.setLong(1, aFeed.getID()); stmt.setLong(2, aList.getID()); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(MessageFormat.format( Strings.error("db.feed.was.not.linked.to.reading.list"), aFeed, aList)); context.removeFeedIfNoRefs(aFeed); } /** * Updates link information between the guide and the feed. * * @param guide guide. * @param feed feed. * * @throws SQLException if DB update failed. */ public void updateFeedLink(StandardGuide guide, IFeed feed) throws SQLException { long guideId = guide.getID(); long feedId = feed.getID(); if (guideId == -1) throw new IllegalStateException(MessageFormat.format( Strings.error("db.guide.is.transient.0"), guide)); if (feedId == -1) throw new IllegalStateException(MessageFormat.format( Strings.error("db.feed.is.transient.0"), feed)); StandardGuide.FeedLinkInfo info = guide.getFeedLinkInfo(feed); if (info != null) { // We don't update feed position here as it's useless PreparedStatement stmt = context.getPreparedStatement( "UPDATE FEEDS2GUIDES SET LASTSYNCTIME=? " + "WHERE GUIDEID=? AND FEEDID=?"); stmt.setLong(1, info.getLastSyncTime()); stmt.setLong(2, guideId); stmt.setLong(3, feedId); int rows = stmt.executeUpdate(); if (rows == 0) throw new SQLException(MessageFormat.format( Strings.error("db.feed.link.was.not.updated.guideid.0.feedid.1"), guideId, feedId)); } } /** * Updates the positions of all feeds in the guide. * * @param guide guide. * * @throws SQLException in case of database error. */ public void updateFeedsPositions(IGuide guide) throws SQLException { long guideId = guide.getID(); if (guideId == -1) throw new IllegalStateException(MessageFormat.format( Strings.error("db.guide.is.transient.0"), guide)); Statements stmts = new Statements(); int cnt = guide.getFeedsCount(); for (int i = 0; i < cnt; i++) { IFeed feed = guide.getFeedAt(i); addUpdateFeedPositionBatch(guide, feed, i, stmts); } stmts.execute(); } /** * Updates the position of feed. * * @param guide guide. * @param feed feed. * * @throws SQLException in case of database error. */ public void updateFeedPosition(IGuide guide, IFeed feed) throws SQLException { Statements stmts = new Statements(); addUpdateFeedPositionBatch(guide, feed, guide.indexOf(feed), stmts); stmts.execute(); } /** * Adds an item to the statements batches. * * @param guide guide. * @param feed feed. * @param position position. * @param stmts statements. * * @throws SQLException in case of database error. */ private void addUpdateFeedPositionBatch(IGuide guide, IFeed feed, int position, Statements stmts) throws SQLException { long guideId = guide.getID(); long feedId = feed.getID(); if (guideId == -1) throw new IllegalStateException(MessageFormat.format( Strings.error("db.guide.is.transient.0"), guide)); if (feedId == -1) throw new IllegalStateException(MessageFormat.format( Strings.error("db.feed.is.transient.0"), feed)); ReadingList[] readingLists = !(feed instanceof DirectFeed) ? null : ((DirectFeed)feed).getReadingLists(); if (readingLists == null || readingLists.length == 0) { // Not a part of reading list stmts.addGuideBatch(guideId, feedId, position); } else { for (ReadingList list : readingLists) { if (list.getParentGuide() == guide) stmts.addReadingListBatch(list.getID(), feedId, position); } } } /** * Simple holder for statements. */ private class Statements { private static final String UPDATE_FEEDS2GUIDES = "UPDATE FEEDS2GUIDES SET POSITN=? WHERE GUIDEID=? AND FEEDID=?"; private static final String UPDATE_FEEDS2READINGLISTS = "UPDATE FEEDS2READINGLISTS SET POSITN=? WHERE READINGLISTID=? AND FEEDID=?"; private PreparedStatement stmtGuides; private PreparedStatement stmtReadingLists; /** * Adds the batch to the statement. * * @param gid guide id. * @param fid feed id. * @param pos position. * * @throws SQLException in case of database error. */ public void addGuideBatch(long gid, long fid, int pos) throws SQLException { if (LOG.isLoggable(Level.FINE)) LOG.fine("Statements.addGuideBatch(" + gid + ", " + fid + ", " + pos + ")"); if (stmtGuides == null) stmtGuides = context.getPreparedStatement(UPDATE_FEEDS2GUIDES); stmtGuides.setInt(1, pos); stmtGuides.setLong(2, gid); stmtGuides.setLong(3, fid); stmtGuides.addBatch(); } /** * Adds the batch to the statement. * * @param rid reading list id. * @param fid feed id. * @param pos position. * * @throws SQLException in case of database error. */ public void addReadingListBatch(long rid, long fid, int pos) throws SQLException { if (LOG.isLoggable(Level.FINE)) LOG.fine("Statements.addReadingListBatch(" + rid + ", " + fid + ", " + pos + ")"); if (stmtReadingLists == null) stmtReadingLists = context.getPreparedStatement(UPDATE_FEEDS2READINGLISTS); stmtReadingLists.setInt(1, pos); stmtReadingLists.setLong(2, rid); stmtReadingLists.setLong(3, fid); stmtReadingLists.addBatch(); } /** * Executes the statements. * * @throws SQLException in case of DB error. */ public void execute() throws SQLException { try { if (stmtGuides != null) stmtGuides.executeBatch(); if (stmtReadingLists != null) stmtReadingLists.executeBatch(); } finally { if (stmtGuides != null) stmtGuides.close(); if (stmtReadingLists != null) stmtReadingLists.close(); } } } }