/*
* Sone - MemoryDatabase.kt - Copyright © 2013–2020 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.AbstractService
import com.google.common.util.concurrent.RateLimiter
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.Sone
import net.pterodactylus.sone.data.allAlbums
import net.pterodactylus.sone.data.allImages
import net.pterodactylus.sone.data.impl.AlbumBuilderImpl
import net.pterodactylus.sone.data.impl.ImageBuilderImpl
import net.pterodactylus.sone.data.newestReplyFirst
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.ifTrue
import net.pterodactylus.sone.utils.unit
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) }, newestReplyFirst)
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
}
sone.allAlbums.let { albums ->
soneAlbums.putAll(sone.id, albums)
albums.forEach { album -> allAlbums[album.id] = album }
}
sone.rootAlbum.allImages.let { images ->
soneImages.putAll(sone.id, images)
images.forEach { image -> 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(newestReplyFirst.reversed())
}
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 }
override fun setPostReplyKnown(postReply: PostReply): Unit =
writeLock.withLock {
knownPostReplies.add(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)
}
}
}