// 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: DirectFeed.java,v 1.43 2007/11/07 17:16:48 spyromus Exp $ // package com.salas.bb.domain; import com.salas.bb.utils.CommonUtils; import com.salas.bb.utils.i18n.Strings; import com.salas.bb.utils.parser.Channel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.URL; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** * Feed which represents data taken from some direct resource on the web -- RSS, RDF or Atom feed. */ public class DirectFeed extends NetworkFeed implements ITaggable { public static final String PROP_XML_URL = "xmlURL"; public static final String PROP_SITE_URL = "siteURL"; public static final String PROP_DEAD = "dead"; public static final String PROP_CUSTOM_AUTHOR = "customAuthor"; public static final String PROP_BASE_AUTHOR = "baseAuthor"; public static final String PROP_AUTHOR = "author"; public static final String PROP_CUSTOM_DESCRIPTION = "customDescription"; public static final String PROP_BASE_DESCRIPTION = "baseDescription"; public static final String PROP_DESCRIPTION = "description"; public static final String PROP_CUSTOM_TITLE = "customTitle"; public static final String PROP_BASE_TITLE = "baseTitle"; public static final String PROP_INLINKS = "inLinks"; public static final String PROP_LAST_METADATA_UPDATE_TIME = "lastMetaDataUpdateTime"; public static final String PROP_READING_LIST = "readingList"; public static final String PROP_DISABLED = "disabled"; public static final String PROP_SYNC_HASH = "syncHash"; /** * Default value for dead-flag. */ public static final boolean DEFAULT_DEAD = false; private String baseTitle; private String baseAuthor; private String baseDescription; private URL xmlURL; private URL siteURL; private String customTitle; private String customDescription; private String customAuthor; private int inLinks; private boolean dead; private FeedMetaDataHolder metaData; private final MetaDataListener metaDataListener; private String[] sharedTags; private String[] userTags; private boolean unsavedUserTags; private String tagsDescription; private String tagsExtended; private int syncHash; /** * Creates direct network feed. */ public DirectFeed() { dead = DEFAULT_DEAD; baseTitle = null; baseDescription = null; baseAuthor = null; metaDataListener = new MetaDataListener(); FeedMetaDataHolder holder = new FeedMetaDataHolder(); holder.setComplete(true); setMetaData(holder); inLinks = -1; userTags = null; sharedTags = null; unsavedUserTags = false; tagsDescription = null; syncHash = 0; } /** * Returns meta-data object assigned to this feed. * * @return meta-data. */ public FeedMetaDataHolder getMetaDataHolder() { return metaData; } /** * Registers meta-data object assigned to this feed. * * @param aMetaData meta-data. */ public void setMetaData(FeedMetaDataHolder aMetaData) { if (metaData != null) { metaData.removePropertyChangeListener(metaDataListener); } metaData = aMetaData; if (metaData != null) { metaData.addPropertyChangeListener(metaDataListener); // Copy some properties to the feed URL newXmlURL = metaData.getXmlURL(); if (xmlURL == null && newXmlURL != null) setXmlURL(newXmlURL); String title = metaData.getTitle(); if (title != null) setBaseTitle(title); if (baseAuthor == null) setBaseAuthor(metaData.getAuthor()); if (baseDescription == null) setBaseDescription(metaData.getDescription()); if (siteURL == null) setSiteURL(metaData.getHtmlURL()); if (inLinks == -1) { Integer inboundLinks = metaData.getInboundLinks(); if (inboundLinks != null) setInLinks(inboundLinks); } } invalidateVisibilityCache(); } /** * Returns the number of inbound links pointing to this feed. * * @return -1 for unresolved, -2 for resolved as "unknown" or positive number for count. */ public int getInLinks() { return inLinks; } /** * Sets number of inbound links (-1 - unresolved, -2 - resolved to be "unknown", * positive number - number of inlinks). * * @param aInLinks number of inlinks. */ public void setInLinks(int aInLinks) { int oldValue = inLinks; inLinks = aInLinks; firePropertyChanged(PROP_INLINKS, new Integer(oldValue), new Integer(inLinks)); } /** * Returns last update time of meta-data. * * @return last update time. */ public long getLastMetaDataUpdateTime() { return metaData == null ? -1 : metaData.getLastUpdateTime(); } /** * Sets the last update time of meta-data. * * @param time last update time of meta-data. */ public void setLastMetaDataUpdateTime(long time) { // It will produce all necessary property change events // as the meta-data object events are being listened by // MetaDataListener. if (metaData != null) metaData.setLastUpdateTime(time); } /** * Gets XML URL. * * @return URL. */ public URL getXmlURL() { return xmlURL; } /** * Sets the XML URL. * * @param url new XML URL. */ public void setXmlURL(URL url) { URL oldXmlURL = xmlURL; if (oldXmlURL != url) xmlURL = CommonUtils.intern(url); firePropertyChanged(PROP_XML_URL, oldXmlURL, xmlURL); setInvalidnessReason(null); setLastPollTime(DEFAULT_LAST_POLL_TIME); setInitTime(INIT_TIME_UNINITIALIZED); } /** * Returns URL of the associated site. * * @return site URL. */ public URL getSiteURL() { return siteURL; } /** * Sets the Site URL. * * @param url new Site URL. */ public void setSiteURL(URL url) { URL oldSiteURL = siteURL; if (oldSiteURL != url) siteURL = CommonUtils.intern(url); firePropertyChanged(PROP_SITE_URL, oldSiteURL, siteURL); } /** * Returns title of feed. * * @return title. */ public String getTitle() { String value = customTitle; if (value == null) value = baseTitle; if (value == null) { URL url = getXmlURL(); value = url == null ? null : url.toString(); } return value; } /** * Returns description of the feed. * * @return description of the feed. */ public String getDescription() { String value = customDescription; if (value == null) value = baseDescription; return value; } /** * Returns the author of the feed. * * @return author of the feed. */ public String getAuthor() { String value = customAuthor; if (value == null) value = baseAuthor; return value; } /** * Returns base title taken from feed. * * @return base title. */ public String getBaseTitle() { return baseTitle; } /** * Sets base title. * * @param aBaseTitle base title. */ public void setBaseTitle(String aBaseTitle) { String oldBaseTitle = baseTitle; String oldTitle = getTitle(); baseTitle = aBaseTitle; String newTitle = getTitle(); firePropertyChanged(PROP_BASE_TITLE, oldBaseTitle, baseTitle); firePropertyChanged(PROP_TITLE, oldTitle, newTitle); } /** * Returns base description. * * @return base description. */ public String getBaseDescription() { return baseDescription; } /** * Sets base description. * * @param aBaseDescription base description. */ public void setBaseDescription(String aBaseDescription) { String oldBaseDescription = baseDescription; String oldDescription = getDescription(); baseDescription = aBaseDescription; String newDescription = getDescription(); firePropertyChanged(PROP_BASE_DESCRIPTION, oldBaseDescription, baseDescription); firePropertyChanged(PROP_DESCRIPTION, oldDescription, newDescription); } /** * Returns base author. * * @return base author. */ public String getBaseAuthor() { return baseAuthor; } /** * Sets base author. * * @param aBaseAuthor base author. */ public void setBaseAuthor(String aBaseAuthor) { String oldBaseAuthor = baseAuthor; String oldAuthor = getAuthor(); baseAuthor = aBaseAuthor; String newAuthor = getAuthor(); firePropertyChanged(PROP_BASE_AUTHOR, oldBaseAuthor, baseAuthor); firePropertyChanged(PROP_AUTHOR, oldAuthor, newAuthor); } /** * Returns <code>TRUE</code> if feed was marked as dead. * * @return <code>TRUE</code> if feed was marked as dead. */ public boolean isDead() { return dead; } /** * Marks/unmarks the feed as dead. * * @param aDead TRUE to mark as dead. */ public void setDead(boolean aDead) { if (dead != aDead) { dead = aDead; firePropertyChanged(PROP_DEAD, !dead, dead); } } /** * Returns TRUE if this feed is updatable, meaning that it's not invalid for some reason and * it's proper time to call <code>update()</code> method. The behaviod may differ if the update * operation was called directly to this particular feed and not as a part of a bigger update * operation (update guide or update all). * * @param direct if TRUE then the update was requested directly (not through guide/set or by * periodic check). */ protected boolean isUpdatable(boolean direct) { return (!isDynamic() || !isDisabled()) && super.isUpdatable(direct); } /** * Updates the feed properties from the channel object. * * @param channel channel object. */ protected void updateFeed(Channel channel) { super.updateFeed(channel); String cTitle = channel.getTitle(); String cDescription = channel.getDescription(); String cAuthor = channel.getAuthor(); URL cSiteURL = channel.getSiteURL(); if (cTitle != null) setBaseTitle(cTitle); if (cDescription != null) setBaseDescription(cDescription); if (cAuthor != null) setBaseAuthor(cAuthor); if (cSiteURL != null) setSiteURL(cSiteURL); } /** * Handles permanent redirection to a new URL. This method is required to be overriden by * sub-classes if they wish to handle redirects. * * @param newXmlURL new URL. */ protected void redirected(URL newXmlURL) { // Do not allow redirection when the feed is connected to some reading list // because we rely on its XML URL when checking for updates. if (getReadingLists().length == 0) setXmlURL(newXmlURL); } // --------------------------------------------------------------------------------------------- // Custom Values // --------------------------------------------------------------------------------------------- /** * Sets custom author name. * * @param aCustomAuthor custom author name. */ public void setCustomAuthor(String aCustomAuthor) { String oldCustomAuthor = customAuthor; String oldAuthor = getAuthor(); customAuthor = aCustomAuthor; String newAuthor = getAuthor(); firePropertyChanged(PROP_CUSTOM_AUTHOR, oldCustomAuthor, customAuthor, true, false); firePropertyChanged(PROP_AUTHOR, oldAuthor, newAuthor); } /** * Returns custom author. * * @return custom author. */ public String getCustomAuthor() { return customAuthor; } /** * Sets custom description for the feed. * * @param aCustomDescription custom description text. */ public void setCustomDescription(String aCustomDescription) { String oldCustomDescription = customDescription; String oldDescription = getDescription(); customDescription = aCustomDescription; String newDescription = getDescription(); firePropertyChanged(PROP_CUSTOM_DESCRIPTION, oldCustomDescription, customDescription, true, false); firePropertyChanged(PROP_DESCRIPTION, oldDescription, newDescription); } /** * Returns custom description. * * @return custom description. */ public String getCustomDescription() { return customDescription; } /** * Sets custom title feed the feed. * * @param aCustomTitle custom title. */ public void setCustomTitle(String aCustomTitle) { String oldCustomTitle = customTitle; String oldTitle = getTitle(); customTitle = aCustomTitle; String newTitle = getTitle(); firePropertyChanged(PROP_CUSTOM_TITLE, oldCustomTitle, customTitle, true, false); firePropertyChanged(PROP_TITLE, oldTitle, newTitle); } /** * Returns custom title. * * @return custom title. */ public String getCustomTitle() { return customTitle; } /** * Returns textual representation of inbound links counter (including messages for the * reserved states). * * @return number of links or status. */ public String getTextualInboundLinks() { String msg; switch (inLinks) { case -2: msg = Strings.message("inlinks.unknown.to.service"); break; case -1: msg = Strings.message("inlinks.awaiting.discovery.finish"); break; default: msg = Integer.toString(inLinks); break; } return msg; } // --------------------------------------------------------------------------------------------- private final List<ReadingList> readingLists = new CopyOnWriteArrayList<ReadingList>(); private boolean disabled; /** * Returns a reading lists this feed belongs to. * * @return reading list association. */ public ReadingList[] getReadingLists() { return readingLists.toArray(new ReadingList[0]); } /** * Adds new reading list association. * * @param list reading list. */ public void addReadingList(ReadingList list) { synchronized (readingLists) { if (!readingLists.contains(list)) readingLists.add(list); } } /** * Removes the reading list association. * * @param list reading list. */ public void removeReadingList(ReadingList list) { synchronized (readingLists) { readingLists.remove(list); } } /** * Removes all reading lists references. */ public void removeAllReadingLists() { synchronized (readingLists) { readingLists.clear(); } } /** * Returns <code>TRUE</code> if this feed is marked as disabled. * * @return <code>TRUE</code> if this feed is marked as disabled. */ public boolean isDisabled() { return disabled; } /** * Disables / enables feed. * * @param aDisabled <code>TRUE</code> to disable feed. */ public void setDisabled(boolean aDisabled) { boolean old = disabled; disabled = aDisabled; firePropertyChanged(PROP_DISABLED, old, disabled, true, true); } /** * Returns <code>TRUE</code> if this feed is assigned to some reading list. * * @return <code>TRUE</code> if this feed is assigned to some reading list. */ public boolean isDynamic() { return readingLists.size() > 0; } /** * Returns simple match key, which can be used to detect similarity of feeds. For example, it's * XML URL for the direct feeds, query type + parameter for the query feeds, serialized search * criteria for the search feeds. * * @return match key. */ public String getMatchKey() { return "DF" + xmlURL; } /** * Sets the ID of the feed. * * @param aId ID of the feed. */ public void setID(long aId) { long oldID = getID(); super.setID(aId); if (aId == -1 && oldID != aId) { // We were removed from the database setMetaData(null); } } // --------------------------------------------------------------------------------------------- /** * Listener of changes in meta-data. */ private class MetaDataListener implements PropertyChangeListener { /** * This method gets called when a bound property is changed. * * @param evt A PropertyChangeEvent object describing the event source and the * property that has changed. */ public void propertyChange(PropertyChangeEvent evt) { String property = evt.getPropertyName(); FeedMetaDataHolder md = (FeedMetaDataHolder)evt.getSource(); if (FeedMetaDataHolder.PROP_COMPLETE.equals(property)) { invalidateVisibilityCache(); } else if (FeedMetaDataHolder.PROP_INBOUND_LINKS.equals(property) && inLinks == -1) { Integer inboundLinks = (Integer)evt.getNewValue(); if (inboundLinks != null) setInLinks(inboundLinks); } else if (FeedMetaDataHolder.PROP_LAST_UPDATE_TIME.equals(property)) { // Refire the event with our own name. firePropertyChanged(PROP_LAST_METADATA_UPDATE_TIME, evt.getOldValue(), evt.getNewValue()); } else if (FeedMetaDataHolder.PROP_INVALID.equals(property)) { String reason = md.isInvalid() == null ? Strings.message("feed.invalidness.reason.undiscovered") : md.isInvalid() ? Strings.message("feed.invalidness.reason.invalid") : null; setInvalidnessReason(reason); } } } // --------------------------------------------------------------------------------------------- // ITaggable implementation // --------------------------------------------------------------------------------------------- /** * Returns shared tags. * * @return shared tags. */ public String[] getSharedTags() { return sharedTags; } /** * Sets shared tags. * * @param tags shared tags. */ public void setSharedTags(String[] tags) { String[] oldSharedTags = sharedTags; sharedTags = tags; firePropertyChanged(PROP_SHARED_TAGS, oldSharedTags, sharedTags); } /** * Returns tags assigned by author. * * @return author tags. */ public String[] getAuthorTags() { return new String[0]; } /** * Returns the name of taggable object type. * * @return type name. */ public String getTaggableTypeName() { return Strings.message("taggable.feed"); } /** * Returns link which can be tagged at the service (BB or third-party). * * @return link or <code>NULL</code> if tagging isn't supported. */ public URL getTaggableLink() { return this.getXmlURL(); } /** * Returns user tags. * * @return user tags. */ public String[] getUserTags() { return userTags; } /** * Sets user tags. * * @param tags user tags. */ public void setUserTags(String[] tags) { String[] oldUserTags = userTags; userTags = tags; firePropertyChanged(PROP_USER_TAGS, oldUserTags, userTags, true, false); } /** * Returns <code>TRUE</code> if this object has unsaved user tags. * * @return <code>TRUE</code> if this object has unsaved user tags. */ public boolean hasUnsavedUserTags() { return unsavedUserTags; } /** * Sets unsaved user tags flag. * * @param unsaved <code>TRUE</code> if this object has unsaved user tags. */ public void setUnsavedUserTags(boolean unsaved) { if (unsavedUserTags != unsaved) { unsavedUserTags = unsaved; firePropertyChanged(PROP_UNSAVED_USER_TAGS, !unsaved, unsaved); } } /** * Returns the description of tags. * * @return description. */ public String getTagsDescription() { return tagsDescription; } /** * Sets new description text. * * @param description new description text. */ public void setTagsDescription(String description) { String oldValue = tagsDescription; tagsDescription = description; firePropertyChanged(PROP_TAGS_DESCRIPTION, oldValue, tagsDescription, true, false); } /** * Returns tags extended description text. * * @return tags extended description text. */ public String getTagsExtended() { return tagsExtended; } /** * Tests new tags extended description text. * * @param extended new extended description. */ public void setTagsExtended(String extended) { String oldValue = tagsExtended; tagsExtended = extended; firePropertyChanged(PROP_TAGS_EXTENDED, oldValue, tagsExtended, true, false); } /** * Returns sync hash. * * @return sync hash. */ public int getSyncHash() { return syncHash; } /** * Sets new sync hash. * * @param hash hash. */ public void setSyncHash(int hash) { int oldHash = syncHash; syncHash = hash; firePropertyChanged(PROP_SYNC_HASH, oldHash, hash, false, false); } /** * Calculates new sync hash basing on its current state. * * @return sync hash. */ public int calcSyncHash() { return xmlURL == null ? 0 : xmlURL.toString().hashCode(); } /** * The part of AND-clause of the isVisible() method. * * @return the subclause. * @see com.salas.bb.domain.NetworkFeed#isVisible */ @Override protected boolean isVisibleSubClause() { return !isDisabled(); } }