/*
* Sone - MemoryDatabase.java - Copyright © 2013 David Roden
*
* 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 3 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, see .
*/
package net.pterodactylus.sone.database.memory;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.FluentIterable.from;
import static java.util.Collections.unmodifiableCollection;
import static net.pterodactylus.sone.data.Reply.TIME_COMPARATOR;
import static net.pterodactylus.sone.data.Sone.LOCAL_SONE_FILTER;
import static net.pterodactylus.sone.data.Sone.toAllAlbums;
import static net.pterodactylus.sone.data.Sone.toAllImages;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.PostReply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.data.impl.AlbumBuilderImpl;
import net.pterodactylus.sone.data.impl.ImageBuilderImpl;
import net.pterodactylus.sone.database.AlbumBuilder;
import net.pterodactylus.sone.database.Database;
import net.pterodactylus.sone.database.DatabaseException;
import net.pterodactylus.sone.database.ImageBuilder;
import net.pterodactylus.sone.database.PostBuilder;
import net.pterodactylus.sone.database.PostDatabase;
import net.pterodactylus.sone.database.PostReplyBuilder;
import net.pterodactylus.sone.database.SoneBuilder;
import net.pterodactylus.sone.database.SoneProvider;
import net.pterodactylus.util.config.Configuration;
import net.pterodactylus.util.config.ConfigurationException;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.util.concurrent.AbstractService;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* Memory-based {@link PostDatabase} implementation.
*
* @author David ‘Bombe’ Roden
*/
@Singleton
public class MemoryDatabase extends AbstractService implements Database {
/** The lock. */
private final ReadWriteLock lock = new ReentrantReadWriteLock();
/** The Sone provider. */
private final SoneProvider soneProvider;
/** The configuration. */
private final Configuration configuration;
private final ConfigurationLoader configurationLoader;
private final Map allSones = new HashMap();
/** All posts by their ID. */
private final Map allPosts = new HashMap();
/** All posts by their Sones. */
private final Multimap sonePosts = HashMultimap.create();
/** Whether posts are known. */
private final Set knownPosts = new HashSet();
/** All post replies by their ID. */
private final Map allPostReplies = new HashMap();
/** Replies sorted by Sone. */
private final SortedSetMultimap sonePostReplies = TreeMultimap.create(new Comparator() {
@Override
public int compare(String leftString, String rightString) {
return leftString.compareTo(rightString);
}
}, TIME_COMPARATOR);
/** Whether post replies are known. */
private final Set knownPostReplies = new HashSet();
private final Map allAlbums = new HashMap();
private final Multimap soneAlbums = HashMultimap.create();
private final Map allImages = new HashMap();
private final Multimap soneImages = HashMultimap.create();
private final MemoryBookmarkDatabase memoryBookmarkDatabase;
/**
* Creates a new memory database.
*
* @param soneProvider
* The Sone provider
* @param configuration
* The configuration for loading and saving elements
*/
@Inject
public MemoryDatabase(SoneProvider soneProvider, Configuration configuration) {
this.soneProvider = soneProvider;
this.configuration = configuration;
this.configurationLoader = new ConfigurationLoader(configuration);
memoryBookmarkDatabase =
new MemoryBookmarkDatabase(this, configurationLoader);
}
//
// DATABASE METHODS
//
/**
* Saves the database.
*
* @throws DatabaseException
* if an error occurs while saving
*/
@Override
public void save() throws DatabaseException {
saveKnownPosts();
saveKnownPostReplies();
}
//
// SERVICE METHODS
//
/** {@inheritDocs} */
@Override
protected void doStart() {
memoryBookmarkDatabase.start();
loadKnownPosts();
loadKnownPostReplies();
notifyStarted();
}
/** {@inheritDocs} */
@Override
protected void doStop() {
try {
memoryBookmarkDatabase.stop();
save();
notifyStopped();
} catch (DatabaseException de1) {
notifyFailed(de1);
}
}
@Override
public SoneBuilder newSoneBuilder() {
return new MemorySoneBuilder();
}
@Override
public void storeSone(Sone sone) {
lock.writeLock().lock();
try {
removeSone(sone);
allSones.put(sone.getId(), sone);
sonePosts.putAll(sone.getId(), sone.getPosts());
for (Post post : sone.getPosts()) {
allPosts.put(post.getId(), post);
}
sonePostReplies.putAll(sone.getId(), sone.getReplies());
for (PostReply postReply : sone.getReplies()) {
allPostReplies.put(postReply.getId(), postReply);
}
soneAlbums.putAll(sone.getId(), toAllAlbums.apply(sone));
for (Album album : toAllAlbums.apply(sone)) {
allAlbums.put(album.getId(), album);
}
soneImages.putAll(sone.getId(), toAllImages.apply(sone));
for (Image image : toAllImages.apply(sone)) {
allImages.put(image.getId(), image);
}
} finally {
lock.writeLock().unlock();
}
}
@Override
public void removeSone(Sone sone) {
lock.writeLock().lock();
try {
allSones.remove(sone.getId());
Collection removedPosts = sonePosts.removeAll(sone.getId());
for (Post removedPost : removedPosts) {
allPosts.remove(removedPost.getId());
}
Collection removedPostReplies =
sonePostReplies.removeAll(sone.getId());
for (PostReply removedPostReply : removedPostReplies) {
allPostReplies.remove(removedPostReply.getId());
}
Collection removedAlbums =
soneAlbums.removeAll(sone.getId());
for (Album removedAlbum : removedAlbums) {
allAlbums.remove(removedAlbum.getId());
}
Collection removedImages =
soneImages.removeAll(sone.getId());
for (Image removedImage : removedImages) {
allImages.remove(removedImage.getId());
}
} finally {
lock.writeLock().unlock();
}
}
@Override
public Optional getSone(String soneId) {
lock.readLock().lock();
try {
return fromNullable(allSones.get(soneId));
} finally {
lock.readLock().unlock();
}
}
@Override
public Collection getSones() {
lock.readLock().lock();
try {
return new HashSet(allSones.values());
} finally {
lock.readLock().unlock();
}
}
@Override
public Collection getLocalSones() {
lock.readLock().lock();
try {
return from(allSones.values()).filter(LOCAL_SONE_FILTER).toSet();
} finally {
lock.readLock().unlock();
}
}
@Override
public Collection getRemoteSones() {
lock.readLock().lock();
try {
return from(allSones.values())
.filter(not(LOCAL_SONE_FILTER)) .toSet();
} finally {
lock.readLock().unlock();
}
}
//
// POSTPROVIDER METHODS
//
/** {@inheritDocs} */
@Override
public Optional getPost(String postId) {
lock.readLock().lock();
try {
return fromNullable(allPosts.get(postId));
} finally {
lock.readLock().unlock();
}
}
/** {@inheritDocs} */
@Override
public Collection getPosts(String soneId) {
return new HashSet(getPostsFrom(soneId));
}
/** {@inheritDocs} */
@Override
public Collection getDirectedPosts(final String recipientId) {
lock.readLock().lock();
try {
return from(sonePosts.values()).filter(new Predicate() {
@Override
public boolean apply(Post post) {
return post.getRecipientId().asSet().contains(recipientId);
}
}).toSet();
} finally {
lock.readLock().unlock();
}
}
//
// POSTBUILDERFACTORY METHODS
//
/** {@inheritDocs} */
@Override
public PostBuilder newPostBuilder() {
return new MemoryPostBuilder(this, soneProvider);
}
//
// POSTSTORE METHODS
//
/** {@inheritDocs} */
@Override
public void storePost(Post post) {
checkNotNull(post, "post must not be null");
lock.writeLock().lock();
try {
allPosts.put(post.getId(), post);
getPostsFrom(post.getSone().getId()).add(post);
} finally {
lock.writeLock().unlock();
}
}
/** {@inheritDocs} */
@Override
public void removePost(Post post) {
checkNotNull(post, "post must not be null");
lock.writeLock().lock();
try {
allPosts.remove(post.getId());
getPostsFrom(post.getSone().getId()).remove(post);
post.getSone().removePost(post);
} finally {
lock.writeLock().unlock();
}
}
//
// POSTREPLYPROVIDER METHODS
//
/** {@inheritDocs} */
@Override
public Optional getPostReply(String id) {
lock.readLock().lock();
try {
return fromNullable(allPostReplies.get(id));
} finally {
lock.readLock().unlock();
}
}
/** {@inheritDocs} */
@Override
public List getReplies(final String postId) {
lock.readLock().lock();
try {
return from(allPostReplies.values())
.filter(new Predicate() {
@Override
public boolean apply(PostReply postReply) {
return postReply.getPostId().equals(postId);
}
}).toSortedList(TIME_COMPARATOR);
} finally {
lock.readLock().unlock();
}
}
//
// POSTREPLYBUILDERFACTORY METHODS
//
/** {@inheritDocs} */
@Override
public PostReplyBuilder newPostReplyBuilder() {
return new MemoryPostReplyBuilder(this, soneProvider);
}
//
// POSTREPLYSTORE METHODS
//
/** {@inheritDocs} */
@Override
public void storePostReply(PostReply postReply) {
lock.writeLock().lock();
try {
allPostReplies.put(postReply.getId(), postReply);
} finally {
lock.writeLock().unlock();
}
}
/** {@inheritDocs} */
@Override
public void removePostReply(PostReply postReply) {
lock.writeLock().lock();
try {
allPostReplies.remove(postReply.getId());
} finally {
lock.writeLock().unlock();
}
}
//
// ALBUMPROVDER METHODS
//
@Override
public Optional getAlbum(String albumId) {
lock.readLock().lock();
try {
return fromNullable(allAlbums.get(albumId));
} finally {
lock.readLock().unlock();
}
}
//
// ALBUMBUILDERFACTORY METHODS
//
@Override
public AlbumBuilder newAlbumBuilder() {
return new AlbumBuilderImpl();
}
//
// ALBUMSTORE METHODS
//
@Override
public void storeAlbum(Album album) {
lock.writeLock().lock();
try {
allAlbums.put(album.getId(), album);
soneAlbums.put(album.getSone().getId(), album);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void removeAlbum(Album album) {
lock.writeLock().lock();
try {
allAlbums.remove(album.getId());
soneAlbums.remove(album.getSone().getId(), album);
} finally {
lock.writeLock().unlock();
}
}
//
// IMAGEPROVIDER METHODS
//
@Override
public Optional getImage(String imageId) {
lock.readLock().lock();
try {
return fromNullable(allImages.get(imageId));
} finally {
lock.readLock().unlock();
}
}
//
// IMAGEBUILDERFACTORY METHODS
//
@Override
public ImageBuilder newImageBuilder() {
return new ImageBuilderImpl();
}
//
// IMAGESTORE METHODS
//
@Override
public void storeImage(Image image) {
lock.writeLock().lock();
try {
allImages.put(image.getId(), image);
soneImages.put(image.getSone().getId(), image);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void removeImage(Image image) {
lock.writeLock().lock();
try {
allImages.remove(image.getId());
soneImages.remove(image.getSone().getId(), image);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void bookmarkPost(Post post) {
memoryBookmarkDatabase.bookmarkPost(post);
}
@Override
public void unbookmarkPost(Post post) {
memoryBookmarkDatabase.unbookmarkPost(post);
}
@Override
public boolean isPostBookmarked(Post post) {
return memoryBookmarkDatabase.isPostBookmarked(post);
}
@Override
public Set getBookmarkedPosts() {
return memoryBookmarkDatabase.getBookmarkedPosts();
}
//
// PACKAGE-PRIVATE METHODS
//
/**
* Returns whether the given post is known.
*
* @param post
* The post
* @return {@code true} if the post is known, {@code false} otherwise
*/
boolean isPostKnown(Post post) {
lock.readLock().lock();
try {
return knownPosts.contains(post.getId());
} finally {
lock.readLock().unlock();
}
}
/**
* Sets whether the given post is known.
*
* @param post
* The post
* @param known
* {@code true} if the post is known, {@code false} otherwise
*/
void setPostKnown(Post post, boolean known) {
lock.writeLock().lock();
try {
if (known) {
knownPosts.add(post.getId());
} else {
knownPosts.remove(post.getId());
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Returns whether the given post reply is known.
*
* @param postReply
* The post reply
* @return {@code true} if the given post reply is known, {@code false}
* otherwise
*/
boolean isPostReplyKnown(PostReply postReply) {
lock.readLock().lock();
try {
return knownPostReplies.contains(postReply.getId());
} finally {
lock.readLock().unlock();
}
}
/**
* Sets whether the given post reply is known.
*
* @param postReply
* The post reply
* @param known
* {@code true} if the post reply is known, {@code false} otherwise
*/
void setPostReplyKnown(PostReply postReply, boolean known) {
lock.writeLock().lock();
try {
if (known) {
knownPostReplies.add(postReply.getId());
} else {
knownPostReplies.remove(postReply.getId());
}
} finally {
lock.writeLock().unlock();
}
}
//
// PRIVATE METHODS
//
/**
* Gets all posts for the given Sone, creating a new collection if there is
* none yet.
*
* @param soneId
* The ID of the Sone to get the posts for
* @return All posts
*/
private Collection getPostsFrom(String soneId) {
lock.readLock().lock();
try {
return sonePosts.get(soneId);
} finally {
lock.readLock().unlock();
}
}
/** Loads the known posts. */
private void loadKnownPosts() {
Set knownPosts = configurationLoader.loadKnownPosts();
lock.writeLock().lock();
try {
this.knownPosts.clear();
this.knownPosts.addAll(knownPosts);
} finally {
lock.writeLock().unlock();
}
}
/**
* Saves the known posts to the configuration.
*
* @throws DatabaseException
* if a configuration error occurs
*/
private void saveKnownPosts() throws DatabaseException {
lock.readLock().lock();
try {
int postCounter = 0;
for (String knownPostId : knownPosts) {
configuration.getStringValue("KnownPosts/" + postCounter++ + "/ID").setValue(knownPostId);
}
configuration.getStringValue("KnownPosts/" + postCounter + "/ID").setValue(null);
} catch (ConfigurationException ce1) {
throw new DatabaseException("Could not save database.", ce1);
} finally {
lock.readLock().unlock();
}
}
/** Loads the known post replies. */
private void loadKnownPostReplies() {
Set knownPostReplies = configurationLoader.loadKnownPostReplies();
lock.writeLock().lock();
try {
this.knownPostReplies.clear();
this.knownPostReplies.addAll(knownPostReplies);
} finally {
lock.writeLock().unlock();
}
}
/**
* Saves the known post replies to the configuration.
*
* @throws DatabaseException
* if a configuration error occurs
*/
private void saveKnownPostReplies() throws DatabaseException {
lock.readLock().lock();
try {
int replyCounter = 0;
for (String knownReplyId : knownPostReplies) {
configuration.getStringValue("KnownReplies/" + replyCounter++ + "/ID").setValue(knownReplyId);
}
configuration.getStringValue("KnownReplies/" + replyCounter + "/ID").setValue(null);
} catch (ConfigurationException ce1) {
throw new DatabaseException("Could not save database.", ce1);
} finally {
lock.readLock().unlock();
}
}
}