/* * 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.* 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) } } }