/* ********************************************************************** **
** Copyright notice **
** **
** (c) 2005-2009 RSSOwl Development Team **
** http://www.rssowl.org/ **
** **
** All rights reserved **
** **
** This program and the accompanying materials are made available under **
** the terms of the Eclipse Public License v1.0 which accompanies this **
** distribution, and is available at: **
** http://www.rssowl.org/legal/epl-v10.html **
** **
** A copy is found in the file epl-v10.html and important notices to the **
** license from the team is found in the textfile LICENSE.txt distributed **
** in this package. **
** **
** This copyright notice MUST APPEAR in all copies of the file! **
** **
** Contributors: **
** RSSOwl Development Team - initial API and implementation **
** **
** ********************************************************************** */
package org.rssowl.core.internal.persist;
import org.eclipse.core.runtime.Assert;
import org.rssowl.core.persist.IAttachment;
import org.rssowl.core.persist.ICategory;
import org.rssowl.core.persist.IFeed;
import org.rssowl.core.persist.IGuid;
import org.rssowl.core.persist.ILabel;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.IPerson;
import org.rssowl.core.persist.ISource;
import org.rssowl.core.persist.reference.FeedLinkReference;
import org.rssowl.core.persist.reference.NewsReference;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.core.util.MergeUtils;
import org.rssowl.core.util.StringUtils;
import org.rssowl.core.util.SyncUtils;
import java.io.Serializable;
import java.net.URI;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* A News is a single entry inside a Feed. The attributes IsRead, IsNew and
* IsDeleted describe the life-cycle of a News:
* <ul>
* <li>IsRead: The News has been marked read</li>
* <li>IsNew: The News has not been read and was not yet looked at</li>
* <li>IsDeleted: The News has been deleted by the user or system</li>
* </ul>
*
* @author bpasero
*/
public class News extends AbstractEntity implements INews {
/* A Lock used for read/write access of the News getters and setters */
static final class Lock {
private final transient ReentrantReadWriteLock fLock = new ReentrantReadWriteLock();
private volatile transient Thread fReadLockThread;
void acquireWriteLock() {
if (fReadLockThread == Thread.currentThread()) {
throw new IllegalStateException("Cannot acquire the write lock from the " + //$NON-NLS-1$
"same thread as the read lock."); //$NON-NLS-1$
}
fLock.writeLock().lock();
}
void releaseWriteLock() {
fLock.writeLock().unlock();
}
void acquireReadLock() {
fLock.readLock().lock();
}
void acquireReadLockSpecial() {
fLock.readLock().lock();
fReadLockThread = Thread.currentThread();
}
void releaseReadLock() {
fLock.readLock().unlock();
}
void releaseReadLockSpecial() {
fReadLockThread = null;
fLock.readLock().unlock();
}
}
private String fTitle;
private String fLinkText;
private String fBaseUri;
private Date fReceiveDate;
private Date fPublishDate;
private Date fModifiedDate;
private String fComments;
private String fInReplyTo;
private boolean fIsFlagged;
private int fRating;
private int fStateOrdinal = INews.State.NEW.ordinal();
private String fGuidValue;
private transient IGuid fGuid;
private boolean fGuidIsPermaLink;
private ISource fSource;
private String fFeedLink;
private IPerson fAuthor;
private List<IAttachment> fAttachments;
private List<ICategory> fCategories;
private Set<ILabel> fLabels;
/* This field is only non-zero if the parent is not a feed */
private long fParentId;
/* We can't use fDescription to support migration from M7 to M8 */
private transient String fTransientDescription;
private transient boolean fTransientDescriptionSet;
private transient final Lock fLock = new Lock();
/**
* Constructor used by <code>DefaultModelFactory</code>
*
* @param feed The Feed this News is belonging to.
*/
public News(IFeed feed) {
super(null);
Assert.isNotNull(feed, "The type News requires a Feed that is not NULL"); //$NON-NLS-1$
fFeedLink = feed.getLink().toString();
fReceiveDate = new Date();
init();
}
/**
* Creates a new Element of the Type News
*
* @param id The unique id of the News.
* @param feed The Feed this News belongs to.
* @param receiveDate The Date this News was received.
*/
public News(Long id, IFeed feed, Date receiveDate) {
super(id);
Assert.isNotNull(feed, "The type News requires a Feed that is not NULL"); //$NON-NLS-1$
fFeedLink = feed.getLink().toString();
Assert.isNotNull(receiveDate, "The type News requires a ReceiveDate that is not NULL"); //$NON-NLS-1$
fReceiveDate = receiveDate;
init();
}
/**
* @param news the news to copy the values from
* @param parentId the container of the news (typically a news bin)
*/
public News(News news, long parentId) {
super(null, news);
fParentId = parentId;
news.fLock.acquireReadLock();
try {
for (IAttachment attachment : news.getAttachments())
addAttachment(new Attachment(attachment, this));
if (news.getAuthor() != null)
fAuthor = new Person(news.getAuthor());
fBaseUri = news.fBaseUri;
for (ICategory category : news.getCategories())
addCategory(new Category(category));
setDescription(news.getDescription());
fComments = news.fComments;
fFeedLink = news.fFeedLink;
setGuid(news.getGuid());
fInReplyTo = news.fInReplyTo;
fIsFlagged = news.fIsFlagged;
/* Don't need to copy the labels because the relationship is ManyToMany. */
fLabels = news.fLabels == null ? null : new HashSet<ILabel>(news.fLabels);
fLinkText = news.fLinkText;
if (news.fModifiedDate != null)
fModifiedDate = new Date(news.fModifiedDate.getTime());
if (news.fPublishDate != null)
fPublishDate = new Date(news.fPublishDate.getTime());
fRating = news.fRating;
if (news.fReceiveDate != null)
fReceiveDate = new Date(news.fReceiveDate.getTime());
if (news.getSource() != null)
fSource = new Source(news.getSource());
fStateOrdinal = news.fStateOrdinal;
fTitle = news.fTitle;
} finally {
news.fLock.releaseReadLock();
}
init();
}
/**
* Default constructor for deserialization
*/
protected News() {
// As per javadoc
}
/**
* Initialises object after deserialization. Should not be used otherwise.
*/
public final void init() {
fLock.acquireWriteLock();
try {
if (fGuidValue != null)
fGuid = new Guid(fGuidValue, fGuidIsPermaLink);
} finally {
fLock.releaseWriteLock();
}
}
/**
* Acquires the read lock used by all non-mutating public methods of this
* object. This method also ensures that an IllegalStateException is thrown if
* the same thread tries to acquire the write lock (by calling one of the
* mutating methods) while still holding this read lock (to prevent
* deadlocks).
* <p>
* This method should only be used in very specific circumstances. Avoid if
* possible.
* </p>
*
* @see #releaseReadLockSpecial()
*/
public final void acquireReadLockSpecial() {
fLock.acquireReadLockSpecial();
}
/**
* Releases the read lock acquired by calling
* {@link #acquireReadLockSpecial()}. It's very important to _always_ call
* this method after calling acquireReadLockSpecial. Typically this is
* achieved with a try/finally block.
*
* @see #acquireReadLockSpecial()
*/
public final void releaseReadLockSpecial() {
fLock.releaseReadLockSpecial();
}
private <T> Boolean isEquivalentCompare(T o1, T o2) {
if ((o1 == null) && (o2 == null))
return null;
return Boolean.valueOf(equals(o1, o2));
}
/*
* @see org.rssowl.core.internal.persist.AbstractEntity#getProperties()
*/
@Override
@SuppressWarnings("all")
public Map<String, Serializable> getProperties() {
fLock.acquireReadLock();
try {
return super.getProperties();
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.internal.persist.AbstractEntity#getProperty(java.lang.String)
*/
@Override
@SuppressWarnings("all")
public Object getProperty(String key) {
fLock.acquireReadLock();
try {
return super.getProperty(key);
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.internal.persist.AbstractEntity#removeProperty(java.lang.String)
*/
@Override
@SuppressWarnings("all")
public Object removeProperty(String key) {
fLock.acquireWriteLock();
try {
return super.removeProperty(key);
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.internal.persist.AbstractEntity#setProperty(java.lang.String, java.lang.Object)
*/
@Override
@SuppressWarnings("all")
public void setProperty(String key, Serializable value) {
fLock.acquireWriteLock();
try {
super.setProperty(key, value);
} finally {
fLock.releaseWriteLock();
}
}
private boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
/*
* @see org.rssowl.core.persist.INews#isEquivalent(org.rssowl.core.persist.INews)
*/
public boolean isEquivalent(INews o) {
News other = (News) o;
fLock.acquireReadLock();
other.fLock.acquireReadLock();
try {
Assert.isNotNull(other, "other cannot be null"); //$NON-NLS-1$
Boolean guidMatch = isEquivalentCompare(slashTrim(fGuidValue), slashTrim(other.fGuidValue));
//TODO Consider simplifying this. The case where one news has permaLink == true and the other has permaLink == false with the
//same guidValue should not happen in practice.
if (guidMatch != null && guidMatch.equals(Boolean.FALSE) && (fGuidValue == null || fGuidIsPermaLink) && (other.fGuidValue == null || other.fGuidIsPermaLink))
return false;
else if (guidMatch != null && guidMatch.equals(Boolean.TRUE))
return true;
Boolean linkMatch = isEquivalentCompare(slashTrim(fLinkText), slashTrim(other.fLinkText));
if (linkMatch != null) {
if (linkMatch.equals(Boolean.TRUE))
return true;
return false;
}
if (!fFeedLink.equals(other.fFeedLink))
return false;
Boolean titleMatch = isEquivalentCompare(fTitle, other.fTitle);
if (titleMatch != null && titleMatch.equals(Boolean.TRUE))
return true;
return false;
} finally {
fLock.releaseReadLock();
other.fLock.releaseReadLock();
}
}
private String slashTrim(String str) {
if (StringUtils.isSet(str) && str.length() > 1 && str.charAt(str.length() - 1) == '/')
return str.substring(0, str.length() - 1);
return str;
}
/*
* @see
* org.rssowl.core.model.types.INews#addAttachment(org.rssowl.core.model.types
* .IAttachment)
*/
public void addAttachment(IAttachment attachment) {
Assert.isNotNull(attachment, "Exception adding NULL as Attachment into News"); //$NON-NLS-1$
fLock.acquireWriteLock();
try {
if (fAttachments == null)
fAttachments = new ArrayList<IAttachment>(1);
/* Rule: Child needs to know about its new parent already! */
Assert.isTrue(equals(attachment.getNews()), "The Attachment has a different News set!"); //$NON-NLS-1$
fAttachments.add(attachment);
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.persist.INews#getLabels()
*/
public Set<ILabel> getLabels() {
fLock.acquireReadLock();
try {
if (fLabels == null)
return new HashSet<ILabel>(0);
/* Bug: For some reason a label can become null when it was deleted, ignore null thereby */
Set<ILabel> labels= new HashSet<ILabel>(fLabels.size());
Iterator<ILabel> iterator = fLabels.iterator();
while(iterator.hasNext()) {
ILabel label = iterator.next();
if (label != null)
labels.add(label);
}
return labels;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.persist.INews#addLabel(org.rssowl.core.persist.ILabel)
*/
public boolean addLabel(ILabel label) {
Assert.isNotNull(label, "label"); //$NON-NLS-1$
fLock.acquireWriteLock();
try {
if (fLabels == null)
fLabels = new HashSet<ILabel>(1);
return fLabels.add(label);
} finally {
fLock.releaseWriteLock();
}
}
void clearLabels() {
fLock.acquireWriteLock();
try {
if (fLabels == null)
return;
fLabels.clear();
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.persist.INews#removeLabel(org.rssowl.core.persist.ILabel)
*/
public boolean removeLabel(ILabel label) {
Assert.isNotNull(label, "label"); //$NON-NLS-1$
fLock.acquireWriteLock();
try {
if (fLabels == null)
return false;
return fLabels.remove(label);
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getAttachments()
*/
public List<IAttachment> getAttachments() {
fLock.acquireReadLock();
try {
if (fAttachments == null)
return new ArrayList<IAttachment>(0);
return new ArrayList<IAttachment>(fAttachments);
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getAuthor()
*/
public IPerson getAuthor() {
fLock.acquireReadLock();
try {
return fAuthor;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setAuthor(org.rssowl.core.model.types.IPerson)
*/
public void setAuthor(IPerson author) {
fLock.acquireWriteLock();
try {
fAuthor = author;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getDescription()
*/
public String getDescription() {
fLock.acquireReadLock();
try {
if (fTransientDescriptionSet)
return fTransientDescription;
} finally {
fLock.releaseReadLock();
}
if (getId() == null)
return null;
Description description = loadDescription();
return description == null ? null : description.getValue();
}
/*
* @see org.rssowl.core.persist.INews#setDescription(java.lang.String)
*/
public void setDescription(String description) {
fLock.acquireWriteLock();
try {
fTransientDescription = description;
fTransientDescriptionSet = true;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getLink()
*/
public URI getLink() {
fLock.acquireReadLock();
try {
return fLinkText == null ? null : createURI(fLinkText);
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setLink(java.lang.String)
*/
public void setLink(URI link) {
fLock.acquireWriteLock();
try {
fLinkText = link == null ? null : link.toString();
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getPublishDate()
*/
public Date getPublishDate() {
fLock.acquireReadLock();
try {
return fPublishDate;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setPublishDate(java.util.Date)
*/
public void setPublishDate(Date publishDate) {
fLock.acquireWriteLock();
try {
fPublishDate = publishDate;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getTitle()
*/
public String getTitle() {
fLock.acquireReadLock();
try {
return fTitle;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setTitle(java.lang.String)
*/
public void setTitle(String title) {
fLock.acquireWriteLock();
try {
fTitle = title;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getFeed()
*/
public FeedLinkReference getFeedReference() {
fLock.acquireReadLock();
try {
return fFeedLink == null ? null : new FeedLinkReference(createURI(fFeedLink));
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setReceiveDate(java.util.Date)
*/
public void setReceiveDate(Date receiveDate) {
fLock.acquireWriteLock();
try {
fReceiveDate = receiveDate;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getReceiveDate()
*/
public Date getReceiveDate() {
fLock.acquireReadLock();
try {
return fReceiveDate;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setComments(java.lang.String)
*/
public void setComments(String comments) {
fLock.acquireWriteLock();
try {
fComments = comments;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setGuid(org.rssowl.core.model.types.IGuid)
*/
public void setGuid(IGuid guid) {
fLock.acquireWriteLock();
try {
fGuid = guid;
fGuidValue = (guid == null ? null : guid.getValue());
fGuidIsPermaLink = (guid == null ? false : guid.isPermaLink());
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setSource(org.rssowl.core.model.types.ISource)
*/
public void setSource(ISource source) {
fLock.acquireWriteLock();
try {
fSource = source;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setInReplyTo(java.lang.String)
*/
public void setInReplyTo(String guid) {
fLock.acquireWriteLock();
try {
fInReplyTo = guid;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setModifiedDate(java.util.Date)
*/
public void setModifiedDate(Date modifiedDate) {
fLock.acquireWriteLock();
try {
fModifiedDate = modifiedDate;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getModifiedDate()
*/
public Date getModifiedDate() {
fLock.acquireReadLock();
try {
return fModifiedDate;
} finally {
fLock.releaseReadLock();
}
}
/**
* Provides lock free fast access to the date of this news so that algorithms
* with O(n^2) are scaling well.
*
* @return Either Modified-Date, Publish-Date or Received-Date if the formers
* are NULL.
*/
public Date fastGetRecentDate() {
if (fModifiedDate != null)
return fModifiedDate;
if (fPublishDate != null)
return fPublishDate;
return fReceiveDate;
}
/*
* @see org.rssowl.core.model.types.INews#addCategory(org.rssowl.core.model.types.ICategory)
*/
public void addCategory(ICategory category) {
fLock.acquireWriteLock();
try {
if (fCategories == null)
fCategories = new ArrayList<ICategory>(1);
fCategories.add(category);
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getComments()
*/
public String getComments() {
fLock.acquireReadLock();
try {
return fComments;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#isFlagged()
*/
public boolean isFlagged() {
fLock.acquireReadLock();
try {
return fIsFlagged;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setFlagged(boolean)
*/
public void setFlagged(boolean isFlagged) {
fLock.acquireWriteLock();
try {
fIsFlagged = isFlagged;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getGuid()
*/
public IGuid getGuid() {
fLock.acquireReadLock();
try {
return fGuid;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setBase(java.net.URI)
*/
public void setBase(URI baseUri) {
fLock.acquireWriteLock();
try {
fBaseUri = getURIText(baseUri);
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getBase()
*/
public URI getBase() {
fLock.acquireReadLock();
try {
return createURI(fBaseUri);
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getCategories()
*/
public List<ICategory> getCategories() {
fLock.acquireReadLock();
try {
if (fCategories == null)
return new ArrayList<ICategory>(0);
return new ArrayList<ICategory>(fCategories);
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setState(org.rssowl.core.model.types.INews.State)
*/
public void setState(State state) {
Assert.isNotNull(state, "state cannot be null"); //$NON-NLS-1$
fLock.acquireWriteLock();
try {
fStateOrdinal = state.ordinal();
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getState()
*/
public State getState() {
fLock.acquireReadLock();
try {
return INews.State.getState(fStateOrdinal);
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#setRating(int)
*/
public void setRating(int rating) {
fLock.acquireWriteLock();
try {
fRating = rating;
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getRating()
*/
public int getRating() {
fLock.acquireReadLock();
try {
return fRating;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getSource()
*/
public ISource getSource() {
fLock.acquireReadLock();
try {
return fSource;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#getInReplyTo()
*/
public String getInReplyTo() {
fLock.acquireReadLock();
try {
return fInReplyTo;
} finally {
fLock.releaseReadLock();
}
}
/*
* @see org.rssowl.core.model.types.INews#isVisible()
*/
public boolean isVisible() {
INews.State state = getState();
return State.getVisible().contains(state);
}
/*
* @see org.rssowl.core.persist.Reparentable#setParent(java.lang.Object)
*/
public void setParent(IFeed feed) {
Assert.isNotNull(feed, "feed"); //$NON-NLS-1$
fLock.acquireWriteLock();
try {
this.fFeedLink = feed.getLink().toString();
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.persist.INews#removeAttachment(org.rssowl.core.persist.IAttachment)
*/
public void removeAttachment(IAttachment attachment) {
fLock.acquireWriteLock();
try {
if (fAttachments != null)
fAttachments.remove(attachment);
} finally {
fLock.releaseWriteLock();
}
}
/*
* @see org.rssowl.core.persist.INews#getParentId()
*/
public long getParentId() {
fLock.acquireReadLock();
try {
return fParentId;
} finally {
fLock.releaseReadLock();
}
}
/**
* @return the loaded description element with textual content.
*/
public Description loadDescription() {
return new DescriptionReference(this.getIdAsPrimitive()).resolve();
}
/**
* @return the once loaded description element with textual content or
* <code>null</code> if none.
*/
public String getTransientDescription() {
fLock.acquireReadLock();
try {
return fTransientDescription;
} finally {
fLock.releaseReadLock();
}
}
/**
* Removes the description element from this news to free memory.
*/
public void clearTransientDescription() {
fLock.acquireWriteLock();
try {
fTransientDescription = null;
fTransientDescriptionSet = false;
} finally {
fLock.releaseWriteLock();
}
}
/**
* @return <code>true</code> if this news provides a description that is
* loaded and <code>false</code> otherwise.
*/
public boolean isTransientDescriptionSet() {
return fTransientDescriptionSet;
}
/*
* @see org.rssowl.core.persist.INews#getFeedLinkAsText()
*/
public String getFeedLinkAsText() {
return fFeedLink;
}
/*
* @see org.rssowl.core.persist.INews#getLinkAsText()
*/
public String getLinkAsText() {
return fLinkText;
}
/**
* @param news
* @return whether <code>news</code> is identical to this object.
*/
public boolean isIdentical(INews news) {
if (this == news)
return true;
if (!(news instanceof News))
return false;
News n = (News) news;
fLock.acquireReadLock();
n.fLock.acquireReadLock();
try {
return (getId() == null ? n.getId() == null : getId().equals(n.getId())) &&
fFeedLink.equals(n.fFeedLink) &&
simpleFieldsEqual(n) &&
(fReceiveDate == null ? n.fReceiveDate == null : fReceiveDate.equals(n.fReceiveDate)) &&
(getGuid() == null ? n.getGuid() == null : getGuid().equals(n.getGuid())) &&
(fSource == null ? n.fSource == null : fSource.equals(n.fSource)) &&
(fInReplyTo == null ? n.fInReplyTo == null : fInReplyTo.equals(n.fInReplyTo)) &&
(getLabels().equals(n.getLabels())) &&
(getAuthor() == null ? n.getAuthor() == null : getAuthor().equals(n.getAuthor())) &&
getAttachments().equals(n.getAttachments()) &&
getCategories().equals(n.getCategories()) &&
getState() == n.getState() && fIsFlagged == n.fIsFlagged && fRating == n.fRating &&
(getProperties() == null ? n.getProperties() == null : getProperties().equals(n.getProperties()));
} finally {
fLock.releaseReadLock();
n.fLock.releaseReadLock();
}
}
private boolean simpleFieldsEqual(News news) {
return MergeUtils.equals(fBaseUri, news.fBaseUri) &&
MergeUtils.equals(fComments, news.fComments) &&
MergeUtils.equals(fLinkText, news.fLinkText) &&
MergeUtils.equals(fModifiedDate, news.fModifiedDate) &&
MergeUtils.equals(fPublishDate, news.fPublishDate) &&
MergeUtils.equals(fInReplyTo, news.fInReplyTo) &&
MergeUtils.equals(fTitle, news.fTitle);
}
/*
* @see org.rssowl.core.persist.MergeCapable#merge(java.lang.Object)
*/
public MergeResult merge(INews news) {
Assert.isNotNull(news, "news cannot be null"); //$NON-NLS-1$
if (this == news)
Assert.isLegal(this != news, "Trying to merge the same news, this is most likely a mistake, news: " + news); //$NON-NLS-1$
News n = (News) news;
n.fLock.acquireReadLock();
try {
fLock.acquireWriteLock();
try {
boolean isSynchronized = SyncUtils.isSynchronized(this);
boolean wasModified = !MergeUtils.equals(fModifiedDate, n.fModifiedDate) || !MergeUtils.equals(fPublishDate, n.fPublishDate) || !MergeUtils.equals(fTitle, n.fTitle);
/*
* Optimization: Since synchronized feeds typically have hundreds of news every time the feed is loaded, we will only
* merge news if either modified or published date have changed or the articles title. This ensures to keep the computational
* overhead low while still supporting updates to articles that are marked as such.
*/
boolean onlyMergeUserState = isSynchronized && !wasModified;
/* Merge News User State */
boolean updated = mergeState(news);
if (isVisible() && isSynchronized) {
updated |= mergeLabels(n);
updated |= (fIsFlagged != n.fIsFlagged);
fIsFlagged = n.fIsFlagged;
}
/* Merge News Content */
MergeResult newsMergeResult = new MergeResult();
ComplexMergeResult<?> propertiesMergeResult = null;
if (!onlyMergeUserState) {
updated |= processListMergeResult(newsMergeResult, mergeAttachments(n.fAttachments));
updated |= processListMergeResult(newsMergeResult, mergeCategories(n.fCategories));
updated |= processListMergeResult(newsMergeResult, mergeAuthor(n.fAuthor));
updated |= mergeGuid(n.fGuid);
if (wasModified)
mergeDescription(newsMergeResult, n); //Optimization: We only merge in description if the news was modified and indicates this
updated |= processListMergeResult(newsMergeResult, mergeSource(n.fSource));
updated |= !simpleFieldsEqual(n);
fBaseUri = n.fBaseUri;
fComments = n.fComments;
fLinkText = n.fLinkText;
fModifiedDate = n.fModifiedDate;
fPublishDate = n.fPublishDate;
fTitle = n.fTitle;
fInReplyTo = n.fInReplyTo;
propertiesMergeResult = MergeUtils.mergeProperties(this, news);
}
/* Configure News Merge Result based on Merge Results */
if (updated || (propertiesMergeResult != null && propertiesMergeResult.isStructuralChange())) {
newsMergeResult.addUpdatedObject(this);
if (propertiesMergeResult != null)
newsMergeResult.addAll(propertiesMergeResult);
}
return newsMergeResult;
} finally {
fLock.releaseWriteLock();
}
} finally {
n.fLock.releaseReadLock();
}
}
private boolean areEqual(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
private void mergeDescription(MergeResult result, News news) {
String newsDescription = null;
if (news.getId() == null)
newsDescription = news.fTransientDescription;
else
newsDescription = news.getDescription();
Description description = loadDescription();
boolean descriptionUpdated = false;
if (description == null)
description = new Description(this, null);
if (fTransientDescriptionSet && (!areEqual(description.getValue(), fTransientDescription))) {
description.setDescription(fTransientDescription);
descriptionUpdated = true;
}
if (!areEqual(description.getValue(), newsDescription)) {
setDescription(newsDescription);
description.setDescription(newsDescription);
descriptionUpdated = true;
}
if (descriptionUpdated) {
if (description.getValue() == null)
result.addRemovedObject(description);
else
result.addUpdatedObject(description);
}
}
private boolean mergeState(INews news) {
State thisState = getState();
State otherState = getState(news); //Considers special Sync State as needed
if (thisState != otherState && otherState != State.NEW) {
setState(otherState);
return true;
}
if (isUpdated(news)) {
setState(State.UPDATED);
return true;
}
return false;
}
private State getState(INews news) {
if (isVisible() && SyncUtils.isSynchronized(news)) { //Avoid marking a deleted news as visible from a sync merge
if (news.getProperty(SyncUtils.GOOGLE_MARKED_READ) != null)
return State.READ;
if (news.getProperty(SyncUtils.GOOGLE_MARKED_UNREAD) != null)
return State.UNREAD;
}
return news.getState();
}
private boolean isUpdated(INews news) {
State thisState = getState();
if (thisState != State.READ && thisState != State.UNREAD)
return false;
if (SyncUtils.isSynchronized(this))
return false; //Unsupported for synchronized news
String title = news.getTitle();
if (!(fTitle == null ? title == null : fTitle.equals(title)))
return true;
return false;
}
private boolean mergeGuid(IGuid guid) {
if (fGuid == null && guid == null)
return false;
if (fGuid == null || guid == null || (!areGuidsIdentical(fGuid, guid))) {
setGuid(guid);
return true;
}
return false;
}
private boolean areGuidsIdentical(IGuid g0, IGuid g1) {
return g0.getValue().equals(g1.getValue()) && g0.isPermaLink() == g1.isPermaLink();
}
private ComplexMergeResult<ISource> mergeSource(ISource source) {
ComplexMergeResult<ISource> mergeResult = MergeUtils.merge(fSource, source);
fSource = mergeResult.getMergedObject();
return mergeResult;
}
private ComplexMergeResult<IPerson> mergeAuthor(IPerson author) {
ComplexMergeResult<IPerson> mergeResult = MergeUtils.merge(fAuthor, author);
fAuthor = mergeResult.getMergedObject();
return mergeResult;
}
private ComplexMergeResult<List<ICategory>> mergeCategories(List<ICategory> categories) {
if (categories == null)
categories = Collections.emptyList();
Comparator<ICategory> comparator = new Comparator<ICategory>() {
public int compare(ICategory o1, ICategory o2) {
if (o1.getName() == null ? o2.getName() == null : o1.getName().equals(o2.getName())) {
return 0;
}
return -1;
}
};
ComplexMergeResult<List<ICategory>> mergeResult = MergeUtils.merge(fCategories, categories, comparator, null);
fCategories = mergeResult.getMergedObject();
return mergeResult;
}
private ComplexMergeResult<List<IAttachment>> mergeAttachments(List<IAttachment> attachments) {
if (attachments == null)
attachments = Collections.emptyList();
Comparator<IAttachment> comparator = new Comparator<IAttachment>() {
public int compare(IAttachment o1, IAttachment o2) {
if (o1.getLink() == null ? o2.getLink() == null : o1.getLink().equals(o2.getLink())) {
return 0;
}
return -1;
}
};
ComplexMergeResult<List<IAttachment>> mergeResult = MergeUtils.merge(fAttachments, attachments, comparator, this);
fAttachments = mergeResult.getMergedObject();
return mergeResult;
}
private boolean mergeLabels(INews news) {
/* Sort the labels because we can not predict the order */
Set<ILabel> thisLabels = CoreUtils.getSortedLabels(this);
Set<ILabel> otherLabels = CoreUtils.getSortedLabels(news);
/* Identical Equals */
if (Arrays.equals(thisLabels.toArray(), otherLabels.toArray()))
return false;
/* Remove All Labels */
if (otherLabels.isEmpty()) {
clearLabels();
return true;
}
/* Add Specific Labels */
for (ILabel otherLabel : otherLabels) {
if (!thisLabels.contains(otherLabel))
addLabel(otherLabel);
}
/* Remove Specific Labels */
for (ILabel thisLabel : thisLabels) {
if (!otherLabels.contains(thisLabel))
removeLabel(thisLabel);
}
return true;
}
/*
* @see org.rssowl.core.persist.IEntity#toReference()
*/
public NewsReference toReference() {
return new NewsReference(getIdAsPrimitive());
}
/*
* @see org.rssowl.core.internal.persist.AbstractEntity#toString()
*/
@Override
public synchronized String toString() {
StringBuilder str = new StringBuilder();
str.append("\n\n****************************** News ******************************\n"); //$NON-NLS-1$
fLock.acquireReadLock();
try {
str.append("\nNews ID: ").append(getId()); //$NON-NLS-1$
if (getTitle() != null)
str.append("\nTitle: ").append(getTitle()); //$NON-NLS-1$
if (getLinkAsText() != null)
str.append("\nLink: ").append(getLinkAsText()); //$NON-NLS-1$
} finally {
fLock.releaseReadLock();
}
return str.toString();
}
/**
* Returns a String describing the state of this Entity.
*
* @return A String describing the state of this Entity.
*/
public String toLongString() {
StringBuilder str = new StringBuilder();
str.append("\n\n****************************** News ******************************\n"); //$NON-NLS-1$
fLock.acquireReadLock();
try {
str.append("\nNews ID: ").append(getId()); //$NON-NLS-1$
if (fFeedLink != null)
str.append("\nFeed Link: ").append(fFeedLink); //$NON-NLS-1$
str.append("\nState: ").append(getState()); //$NON-NLS-1$
if (getTitle() != null)
str.append("\nTitle: ").append(getTitle()); //$NON-NLS-1$
if (getLinkAsText() != null)
str.append("\nLink: ").append(getLinkAsText()); //$NON-NLS-1$
if (getBase() != null)
str.append("\nBase URI: ").append(getBase()); //$NON-NLS-1$
if (getDescription() != null)
str.append("\nDescription: ").append(getDescription()); //$NON-NLS-1$
str.append("\nRating: ").append(getRating()); //$NON-NLS-1$
if (getPublishDate() != null)
str.append("\nPublish Date: ").append(DateFormat.getDateTimeInstance().format(getPublishDate())); //$NON-NLS-1$
if (getReceiveDate() != null)
str.append("\nReceive Date: ").append(DateFormat.getDateTimeInstance().format(getReceiveDate())); //$NON-NLS-1$
if (getModifiedDate() != null)
str.append("\nModified Date: ").append(DateFormat.getDateTimeInstance().format(getModifiedDate())); //$NON-NLS-1$
if (getAuthor() != null)
str.append("\nAuthor: ").append(getAuthor()); //$NON-NLS-1$
if (getComments() != null)
str.append("\nComments: ").append(getComments()); //$NON-NLS-1$
if (getGuid() != null)
str.append("\nGUID: ").append(getGuid()); //$NON-NLS-1$
if (getSource() != null)
str.append("\nSource: ").append(getSource()); //$NON-NLS-1$
if (getInReplyTo() != null)
str.append("\nIn Reply To: ").append(getInReplyTo()); //$NON-NLS-1$
str.append("\nLabesl: ").append(getLabels()); //$NON-NLS-1$
str.append("\nAttachments: ").append(getAttachments()); //$NON-NLS-1$
str.append("\nCategories: ").append(getCategories()); //$NON-NLS-1$
str.append("\nIs Flagged: ").append(fIsFlagged); //$NON-NLS-1$
str.append("\nProperties: ").append(getProperties()); //$NON-NLS-1$
} finally {
fLock.releaseReadLock();
}
return str.toString();
}
}