/* ********************************************************************** **
** 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.service;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.rssowl.core.Owl;
import org.rssowl.core.internal.Activator;
import org.rssowl.core.internal.InternalOwl;
import org.rssowl.core.internal.persist.BookMark;
import org.rssowl.core.internal.persist.Description;
import org.rssowl.core.internal.persist.Feed;
import org.rssowl.core.internal.persist.LazyList;
import org.rssowl.core.internal.persist.News;
import org.rssowl.core.internal.persist.dao.DAOServiceImpl;
import org.rssowl.core.internal.persist.dao.EntitiesToBeIndexedDAOImpl;
import org.rssowl.core.internal.persist.dao.IDescriptionDAO;
import org.rssowl.core.persist.IBookMark;
import org.rssowl.core.persist.IEntity;
import org.rssowl.core.persist.IFeed;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.INewsBin;
import org.rssowl.core.persist.INewsBin.StatesUpdateInfo;
import org.rssowl.core.persist.IPersistable;
import org.rssowl.core.persist.NewsCounter;
import org.rssowl.core.persist.dao.DAOService;
import org.rssowl.core.persist.dao.DynamicDAO;
import org.rssowl.core.persist.dao.INewsBinDAO;
import org.rssowl.core.persist.dao.INewsCounterDAO;
import org.rssowl.core.persist.event.FeedEvent;
import org.rssowl.core.persist.event.ModelEvent;
import org.rssowl.core.persist.event.NewsBinEvent;
import org.rssowl.core.persist.event.NewsEvent;
import org.rssowl.core.persist.event.runnable.EventRunnable;
import org.rssowl.core.persist.event.runnable.FeedEventRunnable;
import org.rssowl.core.persist.event.runnable.NewsEventRunnable;
import org.rssowl.core.persist.reference.FeedLinkReference;
import org.rssowl.core.persist.reference.NewsReference;
import org.rssowl.core.persist.service.PersistenceException;
import org.rssowl.core.persist.service.UniqueConstraintException;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.ext.Db4oException;
import com.db4o.query.Query;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Helper for DB related tasks.
*/
public final class DBHelper {
static final int BUFFER = 32768;
private DBHelper() {
super();
}
public static void rename(File origin, File destination) throws PersistenceException {
/* Try atomic rename first. If that fails, rely on delete + rename */
if (!origin.renameTo(destination)) {
destination.delete();
if (!origin.renameTo(destination)) {
throw new PersistenceException("Failed to rename: " + origin + " to: " + destination); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
public static String readFirstLineFromFile(File file) {
BufferedReader reader = null;
try {
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8")); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
reader = new BufferedReader(new FileReader(file));
}
String text = reader.readLine();
return text;
} catch (IOException e) {
throw new PersistenceException(e);
} finally {
DBHelper.closeQuietly(reader);
}
}
public static final void copyFileNIO(File originFile, File destinationFile) {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = new FileInputStream(originFile);
FileChannel srcChannel = inputStream.getChannel();
if (!destinationFile.exists())
destinationFile.createNewFile();
outputStream = new FileOutputStream(destinationFile);
FileChannel dstChannel = outputStream.getChannel();
long bytesToTransfer = srcChannel.size();
long position = 0;
while (bytesToTransfer > 0) {
long bytesTransferred = dstChannel.transferFrom(srcChannel, position, bytesToTransfer);
position += bytesTransferred;
bytesToTransfer -= bytesTransferred;
}
} catch (IOException e) {
Activator.getDefault().logError("Failed to copy file using NIO. Falling back to traditional IO", e); //$NON-NLS-1$
copyFileIO(originFile, destinationFile, new NullProgressMonitor());
} finally {
closeQuietly(inputStream);
closeQuietly(outputStream);
}
}
public static void copyFileIO(File originFile, File destinationFile, IProgressMonitor monitor) {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = new FileInputStream(originFile);
if (!destinationFile.exists())
destinationFile.createNewFile();
outputStream = new FileOutputStream(destinationFile);
int i = 0;
byte[] buf = new byte[BUFFER];
while ((i = inputStream.read(buf)) != -1 && !monitor.isCanceled()) {
outputStream.write(buf, 0, i);
monitor.worked(1);
}
} catch (IOException e) {
throw new PersistenceException(e);
} finally {
closeQuietly(inputStream);
closeQuietly(outputStream);
}
}
public static void writeToFile(File file, String text) {
BufferedWriter writer = null;
try {
try {
writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
writer = new BufferedWriter(new FileWriter(file));
}
writer.write(text);
writer.flush();
} catch (IOException e) {
throw new PersistenceException(e);
} finally {
closeQuietly(writer);
}
}
public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
Activator.getDefault().logError("Failed to close stream.", e); //$NON-NLS-1$
}
}
}
public static final List<EventRunnable<?>> cleanUpEvents() {
List<EventRunnable<?>> eventNotifiers = EventsMap.getInstance().removeEventRunnables();
EventsMap.getInstance().removeEventTemplatesMap();
EventManager.getInstance().clear();
return eventNotifiers;
}
public static final void cleanUpAndFireEvents() {
fireEvents(cleanUpEvents());
}
public static final void fireEvents(List<EventRunnable<?>> eventNotifiers) {
if (eventNotifiers == null) {
return;
}
for (EventRunnable<?> runnable : eventNotifiers) {
runnable.run();
}
}
public static final PersistenceException rollbackAndPE(ObjectContainer db, Exception e) {
DBHelper.cleanUpEvents();
db.rollback();
return new PersistenceException(e);
}
public static final void putEventTemplate(ModelEvent modelEvent) {
EventsMap.getInstance().putEventTemplate(modelEvent);
}
public static final void saveFeed(ObjectContainer db, IFeed feed) {
if (feed.getId() == null && feedExists(db, feed.getLink()))
throw new UniqueConstraintException("link", feed); //$NON-NLS-1$
ModelEvent feedEventTemplate = new FeedEvent(feed, true);
DBHelper.putEventTemplate(feedEventTemplate);
saveAndCascadeAllNews(db, feed.getNews(), false);
saveEntities(db, feed.getCategories());
saveEntity(db, feed.getAuthor());
saveEntity(db, feed.getImage());
db.ext().set(feed, 2);
}
private static void saveEntity(ObjectContainer db, IPersistable entity) {
if (entity != null)
db.set(entity);
}
private static void saveEntities(ObjectContainer db, List<? extends IEntity> entities) {
for (IEntity entity : entities)
db.ext().set(entity, 1);
}
static void saveAndCascadeAllNews(ObjectContainer db, Collection<INews> newsCollection, boolean root) {
for (INews news : newsCollection)
((News) news).acquireReadLockSpecial();
try {
for (INews news : newsCollection)
saveAndCascadeNews(db, news, root);
} finally {
for (INews news : newsCollection) {
News n = (News) news;
n.releaseReadLockSpecial();
n.clearTransientDescription();
}
}
}
public static final INews peekPersistedNews(ObjectContainer db, INews news) {
INews oldNews = db.ext().peekPersisted(news, 2, true);
if (oldNews instanceof News)
((News) oldNews).init();
return oldNews;
}
public static final void saveUpdatedNews(ObjectContainer db, INews news) {
INews oldNews = peekPersistedNews(db, news);
if (oldNews != null) {
ModelEvent newsEventTemplate = new NewsEvent(oldNews, news, false, true);
DBHelper.putEventTemplate(newsEventTemplate);
}
db.ext().set(news, 2);
}
static final boolean feedExists(ObjectContainer db, URI link) {
return !getFeeds(db, link).isEmpty();
}
@SuppressWarnings("unchecked")
private static List<Feed> getFeeds(ObjectContainer db, URI link) {
Query query = db.query();
query.constrain(Feed.class);
query.descend("fLinkText").constrain(link.toString()); //$NON-NLS-1$
List<Feed> set = query.execute();
return set;
}
public static final Feed loadFeed(ObjectContainer db, URI link, Integer activationDepth) {
try {
List<Feed> feeds = getFeeds(db, link);
if (!feeds.isEmpty()) {
Feed feed = feeds.iterator().next();
if (activationDepth != null)
db.ext().activate(feed, activationDepth.intValue());
return feed;
}
return null;
} catch (Db4oException e) {
throw new PersistenceException(e);
}
}
public static final boolean existsFeed(ObjectContainer db, URI link) {
try {
List<Feed> feeds = getFeeds(db, link);
return !feeds.isEmpty();
} catch (Db4oException e) {
throw new PersistenceException(e);
}
}
public static final void saveAndCascadeNews(ObjectContainer db, INews news, boolean root) {
INews oldNews = peekPersistedNews(db, news);
if (oldNews != null || root) {
ModelEvent event = new NewsEvent(oldNews, news, root);
putEventTemplate(event);
}
saveEntities(db, news.getCategories());
saveEntity(db, news.getAuthor());
saveEntities(db, news.getAttachments());
saveEntity(db, news.getSource());
db.ext().set(news, 2);
saveDescription(db, news);
}
private static void saveDescription(ObjectContainer db, INews news) {
News n = (News) news;
/*
* Avoid loading from the db if the description of the news being saved has
* not been changed.
*/
if (!n.isTransientDescriptionSet())
return;
Description dbDescription = null;
String dbDescriptionValue = null;
dbDescription = getDescriptionDAO().load(news.getId());
if (dbDescription != null)
dbDescriptionValue = dbDescription.getValue();
String newsDescriptionValue = n.getTransientDescription();
/*
* If the description in the news has been set to null and it's already null
* in the database, there is nothing to do.
*/
if (dbDescriptionValue == null && newsDescriptionValue == null)
return;
else if (dbDescriptionValue == null && newsDescriptionValue != null)
db.set(new Description(news, newsDescriptionValue));
else if (dbDescriptionValue != null && newsDescriptionValue == null)
db.delete(dbDescription);
else if (dbDescriptionValue != null && !dbDescriptionValue.equals(newsDescriptionValue)) {
if (dbDescription != null) {
dbDescription.setDescription(newsDescriptionValue);
db.set(dbDescription);
}
}
}
public static IDescriptionDAO getDescriptionDAO() {
DAOService daoService = InternalOwl.getDefault().getPersistenceService().getDAOService();
if (daoService instanceof DAOServiceImpl)
return ((DAOServiceImpl) daoService).getDescriptionDAO();
throw new IllegalStateException("This method should only be called if DAOService is of type " + DAOServiceImpl.class + ", but it is of type: " + daoService.getClass()); //$NON-NLS-1$ //$NON-NLS-2$
}
public static void preCommit(ObjectContainer db) {
updateNewsCounter(db);
updateNewsToBeIndexed(db);
updateNewsBins(db);
}
public static EntitiesToBeIndexedDAOImpl getEntitiesToBeIndexedDAO() {
DAOService service = InternalOwl.getDefault().getPersistenceService().getDAOService();
if (service instanceof DAOServiceImpl) {
EntitiesToBeIndexedDAOImpl entitiesToBeIndexedDAO = ((DAOServiceImpl) service).getEntitiesToBeIndexedDAO();
return entitiesToBeIndexedDAO;
}
return null;
}
private static void updateNewsToBeIndexed(ObjectContainer db) {
NewsEventRunnable newsEventRunnables = getNewsEventRunnables(EventsMap.getInstance().getEventRunnables());
if (newsEventRunnables == null)
return;
EntitiesToBeIndexedDAOImpl dao = getEntitiesToBeIndexedDAO();
EntityIdsByEventType newsToBeIndexed = dao.load();
Set<NewsEvent> updateEvents = new HashSet<NewsEvent>(newsEventRunnables.getUpdateEvents().size());
Set<NewsEvent> deleteEvents = new HashSet<NewsEvent>(newsEventRunnables.getRemoveEvents());
Set<NewsEvent> persistEvents = filterPersistedNewsForIndexing(newsEventRunnables.getPersistEvents());
for (NewsEvent event : newsEventRunnables.getUpdateEvents())
indexTypeForNewsUpdate(event, persistEvents, updateEvents, deleteEvents);
NewsEventRunnable copy = new NewsEventRunnable();
for (NewsEvent persistEvent : persistEvents)
copy.addCheckedPersistEvent(persistEvent);
for (NewsEvent updateEvent : updateEvents)
copy.addCheckedUpdateEvent(updateEvent);
for (NewsEvent deleteEvent : deleteEvents)
copy.addCheckedRemoveEvent(deleteEvent);
newsToBeIndexed.addAllEntities(copy.getPersistEvents(), copy.getUpdateEvents(), copy.getRemoveEvents());
newsToBeIndexed.compact();
db.ext().set(newsToBeIndexed, Integer.MAX_VALUE);
}
public static Set<NewsEvent> filterPersistedNewsForIndexing(Collection<NewsEvent> events) {
Set<NewsEvent> result = new HashSet<NewsEvent>(events.size());
for (NewsEvent event : events)
if (event.getEntity().isVisible())
result.add(event);
return result;
}
public static void indexTypeForNewsUpdate(NewsEvent event, Collection<NewsEvent> newsToRestore, Collection<NewsEvent> newsToUpdate, Collection<NewsEvent> newsToDelete) {
boolean wasVisible = event.getOldNews().isVisible();
boolean isVisible = event.getEntity().isVisible();
/* News got Deleted/Hidden */
if (wasVisible && !isVisible)
newsToDelete.add(event);
/* News got Restored */
else if (!wasVisible && isVisible)
newsToRestore.add(event);
/* Normal Update */
else if (wasVisible && isVisible)
newsToUpdate.add(event);
}
public static NewsEventRunnable getNewsEventRunnables(List<EventRunnable<?>> eventRunnables) {
for (EventRunnable<?> eventRunnable : eventRunnables) {
if (eventRunnable instanceof NewsEventRunnable)
return (NewsEventRunnable) eventRunnable;
}
return null;
}
private static void updateNewsBins(ObjectContainer db) {
NewsEventRunnable newsEventRunnable = getNewsEventRunnables(EventsMap.getInstance().getEventRunnables());
if (newsEventRunnable == null)
return;
Map<Long, List<StatesUpdateInfo>> statesUpdateInfos = new HashMap<Long, List<StatesUpdateInfo>>(5);
for (NewsEvent newsEvent : newsEventRunnable.getUpdateEvents()) {
INews news = newsEvent.getEntity();
if (news.getParentId() != 0 && (newsEvent.getOldNews().getState() != news.getState())) {
List<StatesUpdateInfo> list = statesUpdateInfos.get(news.getParentId());
if (list == null) {
list = new ArrayList<StatesUpdateInfo>();
statesUpdateInfos.put(news.getParentId(), list);
}
list.add(new StatesUpdateInfo(newsEvent.getOldNews().getState(), news.getState(), news.toReference()));
}
}
for (NewsEvent newsEvent : newsEventRunnable.getPersistEvents()) {
INews news = newsEvent.getEntity();
if (news.getParentId() != 0) {
List<StatesUpdateInfo> list = statesUpdateInfos.get(news.getParentId());
if (list == null) {
list = new ArrayList<StatesUpdateInfo>();
statesUpdateInfos.put(news.getParentId(), list);
}
list.add(new StatesUpdateInfo(null, news.getState(), news.toReference()));
}
}
if (!statesUpdateInfos.isEmpty()) {
Set<FeedLinkReference> removedFeedRefs = new HashSet<FeedLinkReference>();
INewsBinDAO newsBinDAO = DynamicDAO.getDAO(INewsBinDAO.class);
for (Map.Entry<Long, List<StatesUpdateInfo>> mapEntry : statesUpdateInfos.entrySet()) {
INewsBin newsBin = newsBinDAO.load(mapEntry.getKey());
if (newsBin.updateNewsStates(mapEntry.getValue())) {
removeNews(db, removedFeedRefs, newsBin.removeNews(EnumSet.of(INews.State.DELETED)));
putEventTemplate(new NewsBinEvent(newsBin, null, true));
db.ext().set(newsBin, Integer.MAX_VALUE);
}
}
removeFeedsAfterNewsBinUpdate(db, removedFeedRefs);
}
}
static void removeNews(ObjectContainer db, Set<FeedLinkReference> feedRefs, Collection<NewsReference> newsRefs) {
for (NewsReference newsRef : newsRefs) {
INews news = newsRef.resolve();
if (news != null) {
feedRefs.add(news.getFeedReference());
db.delete(news);
}
}
}
static void removeFeedsAfterNewsBinUpdate(ObjectContainer db, Set<FeedLinkReference> removedFeedRefs) {
NewsCounter newsCounter = DynamicDAO.getDAO(INewsCounterDAO.class).load();
boolean changed = false;
for (FeedLinkReference feedRef : removedFeedRefs) {
if ((countBookMarkReference(db, feedRef) == 0) && !feedHasNewsWithCopies(db, feedRef)) {
db.delete(feedRef.resolve());
changed = true;
}
}
if (changed)
db.ext().set(newsCounter, Integer.MAX_VALUE);
}
static int countBookMarkReference(ObjectContainer db, FeedLinkReference feedRef) {
Collection<IBookMark> marks = loadAllBookMarks(db, feedRef);
return marks.size();
}
@SuppressWarnings("unchecked")
public static Collection<IBookMark> loadAllBookMarks(ObjectContainer db, FeedLinkReference feedRef) {
Query query = db.query();
query.constrain(BookMark.class);
query.descend("fFeedLink").constrain(feedRef.getLink().toString()); //$NON-NLS-1$
return query.execute();
}
static boolean feedHasNewsWithCopies(ObjectContainer db, FeedLinkReference feedRef) {
Query query = db.query();
query.constrain(News.class);
query.descend("fFeedLink").constrain(feedRef.getLink().toString()); //$NON-NLS-1$
query.descend("fParentId").constrain(0).not(); //$NON-NLS-1$
return !query.execute().isEmpty();
}
public static void updateNewsCounter(ObjectContainer db) {
List<EventRunnable<?>> eventRunnables = EventsMap.getInstance().getEventRunnables();
NewsCounterService newsCounterService = new NewsCounterService(Owl.getPersistenceService().getDAOService().getNewsCounterDAO(), db);
NewsEventRunnable newsEventRunnable = getNewsEventRunnables(eventRunnables);
if (newsEventRunnable != null) {
newsCounterService.onNewsAdded(newsEventRunnable.getPersistEvents());
newsCounterService.onNewsRemoved((newsEventRunnable.getRemoveEvents()));
newsCounterService.onNewsUpdated(newsEventRunnable.getUpdateEvents());
}
for (EventRunnable<?> eventRunnable : eventRunnables) {
if (eventRunnable instanceof FeedEventRunnable) {
FeedEventRunnable feedEventRunnable = (FeedEventRunnable) eventRunnable;
newsCounterService.onFeedRemoved(feedEventRunnable.getRemoveEvents());
break;
}
}
}
public static Collection<IFeed> loadAllFeeds(ObjectContainer db) {
ObjectSet<? extends IFeed> entities = db.query(Feed.class);
return new LazyList<IFeed>(entities, db);
}
}