/*
* Sone - MemoryDatabase.kt - Copyright © 2013–2019 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 com.google.common.base.Preconditions.checkNotNull
import com.google.common.collect.HashMultimap
import com.google.common.collect.Multimap
import com.google.common.collect.TreeMultimap
import com.google.common.util.concurrent.*
import com.google.inject.Inject
import com.google.inject.Singleton
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.Reply.TIME_COMPARATOR
import net.pterodactylus.sone.data.Sone
import net.pterodactylus.sone.data.Sone.toAllAlbums
import net.pterodactylus.sone.data.Sone.toAllImages
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.utils.*
import net.pterodactylus.util.config.Configuration
import net.pterodactylus.util.config.ConfigurationException
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock
/**
* Memory-based [PostDatabase] implementation.
*/
@Singleton
class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
private val lock = ReentrantReadWriteLock()
private val readLock by lazy { lock.readLock()!! }
private val writeLock by lazy { lock.writeLock()!! }
private val configurationLoader = ConfigurationLoader(configuration)
private val allSones = mutableMapOf()
private val allPosts = mutableMapOf()
private val sonePosts: Multimap = HashMultimap.create()
private val knownPosts = mutableSetOf()
private val allPostReplies = mutableMapOf()
private val sonePostReplies: Multimap = TreeMultimap.create(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, TIME_COMPARATOR)
private val knownPostReplies = mutableSetOf()
private val allAlbums = mutableMapOf()
private val soneAlbums: Multimap = HashMultimap.create()
private val allImages = mutableMapOf()
private val soneImages: Multimap = HashMultimap.create()
private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader)
private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader)
private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0)
private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0)
private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0)
override val soneLoader get() = this::getSone
override val sones get() = readLock.withLock { allSones.values.toSet() }
override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
override fun save() {
if (saveRateLimiter.tryAcquire()) {
saveKnownPosts()
saveKnownPostReplies()
}
}
override fun doStart() {
memoryBookmarkDatabase.start()
loadKnownPosts()
loadKnownPostReplies()
notifyStarted()
}
override fun doStop() {
try {
memoryBookmarkDatabase.stop()
save()
notifyStopped()
} catch (de1: DatabaseException) {
notifyFailed(de1)
}
}
override fun newSoneBuilder() = MemorySoneBuilder(this)
override fun storeSone(sone: Sone) {
writeLock.withLock {
removeSone(sone)
allSones[sone.id] = sone
sonePosts.putAll(sone.id, sone.posts)
for (post in sone.posts) {
allPosts[post.id] = post
}
sonePostReplies.putAll(sone.id, sone.replies)
for (postReply in sone.replies) {
allPostReplies[postReply.id] = postReply
}
soneAlbums.putAll(sone.id, toAllAlbums.apply(sone)!!)
for (album in toAllAlbums.apply(sone)!!) {
allAlbums[album.id] = album
}
soneImages.putAll(sone.id, toAllImages.apply(sone)!!)
for (image in toAllImages.apply(sone)!!) {
allImages[image.id] = image
}
}
}
override fun removeSone(sone: Sone) {
writeLock.withLock {
allSones.remove(sone.id)
val removedPosts = sonePosts.removeAll(sone.id)
for (removedPost in removedPosts) {
allPosts.remove(removedPost.id)
}
val removedPostReplies = sonePostReplies.removeAll(sone.id)
for (removedPostReply in removedPostReplies) {
allPostReplies.remove(removedPostReply.id)
}
val removedAlbums = soneAlbums.removeAll(sone.id)
for (removedAlbum in removedAlbums) {
allAlbums.remove(removedAlbum.id)
}
val removedImages = soneImages.removeAll(sone.id)
for (removedImage in removedImages) {
allImages.remove(removedImage.id)
}
}
}
override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
override fun getFriends(localSone: Sone): Collection =
if (!localSone.isLocal) {
emptySet()
} else {
memoryFriendDatabase.getFriends(localSone.id)
}
override fun isFriend(localSone: Sone, friendSoneId: String) =
if (!localSone.isLocal) {
false
} else {
memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
}
override fun addFriend(localSone: Sone, friendSoneId: String) {
if (!localSone.isLocal) {
return
}
memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
}
override fun removeFriend(localSone: Sone, friendSoneId: String) {
if (!localSone.isLocal) {
return
}
memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
}
override fun getFollowingTime(friendSoneId: String) =
memoryFriendDatabase.getFollowingTime(friendSoneId)
override fun getPost(postId: String) =
readLock.withLock { allPosts[postId] }
override fun getPosts(soneId: String): Collection =
sonePosts[soneId].toSet()
override fun getDirectedPosts(recipientId: String) =
readLock.withLock {
allPosts.values.filter {
it.recipientId.orNull() == recipientId
}
}
override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
override fun storePost(post: Post) {
checkNotNull(post, "post must not be null")
writeLock.withLock {
allPosts[post.id] = post
sonePosts[post.sone.id].add(post)
}
}
override fun removePost(post: Post) {
checkNotNull(post, "post must not be null")
writeLock.withLock {
allPosts.remove(post.id)
sonePosts[post.sone.id].remove(post)
post.sone.removePost(post)
}
}
override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
override fun getReplies(postId: String) =
readLock.withLock {
allPostReplies.values
.filter { it.postId == postId }
.sortedWith(TIME_COMPARATOR)
}
override fun newPostReplyBuilder(): PostReplyBuilder =
MemoryPostReplyBuilder(this, this)
override fun storePostReply(postReply: PostReply) =
writeLock.withLock {
allPostReplies[postReply.id] = postReply
}
override fun removePostReply(postReply: PostReply) =
writeLock.withLock {
allPostReplies.remove(postReply.id)
}.unit
override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
override fun storeAlbum(album: Album) =
writeLock.withLock {
allAlbums[album.id] = album
soneAlbums.put(album.sone.id, album)
}.unit
override fun removeAlbum(album: Album) =
writeLock.withLock {
allAlbums.remove(album.id)
soneAlbums.remove(album.sone.id, album)
}.unit
override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
override fun storeImage(image: Image): Unit =
writeLock.withLock {
allImages[image.id] = image
soneImages.put(image.sone.id, image)
}
override fun removeImage(image: Image): Unit =
writeLock.withLock {
allImages.remove(image.id)
soneImages.remove(image.sone.id, image)
}
override fun bookmarkPost(post: Post) =
memoryBookmarkDatabase.bookmarkPost(post)
override fun unbookmarkPost(post: Post) =
memoryBookmarkDatabase.unbookmarkPost(post)
override fun isPostBookmarked(post: Post) =
memoryBookmarkDatabase.isPostBookmarked(post)
protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
fun setPostKnown(post: Post, known: Boolean): Unit =
writeLock.withLock {
if (known)
knownPosts.add(post.id)
else
knownPosts.remove(post.id)
saveKnownPosts()
}
protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
fun setPostReplyKnown(postReply: PostReply, known: Boolean): Unit =
writeLock.withLock {
if (known)
knownPostReplies.add(postReply.id)
else
knownPostReplies.remove(postReply.id)
saveKnownPostReplies()
}
private fun loadKnownPosts() =
configurationLoader.loadKnownPosts()
.let {
writeLock.withLock {
knownPosts.clear()
knownPosts.addAll(it)
}
}
private fun saveKnownPosts() =
saveKnownPostsRateLimiter.tryAcquire().ifTrue {
try {
readLock.withLock {
knownPosts.forEachIndexed { index, knownPostId ->
configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
}
configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
}
} catch (ce1: ConfigurationException) {
throw DatabaseException("Could not save database.", ce1)
}
}
private fun loadKnownPostReplies(): Unit =
configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
writeLock.withLock {
this.knownPostReplies.clear()
this.knownPostReplies.addAll(knownPostReplies)
}
}
private fun saveKnownPostReplies() =
saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
try {
readLock.withLock {
knownPostReplies.forEachIndexed { index, knownPostReply ->
configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
}
configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
}
} catch (ce1: ConfigurationException) {
throw DatabaseException("Could not save database.", ce1)
}
}
}