X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fkotlin%2Fnet%2Fpterodactylus%2Fsone%2Fdatabase%2Fmemory%2FMemoryDatabase.kt;fp=src%2Fmain%2Fkotlin%2Fnet%2Fpterodactylus%2Fsone%2Fdatabase%2Fmemory%2FMemoryDatabase.kt;h=8722873aa0e7a4740a017fb1ad073ed17c7d26d1;hp=0000000000000000000000000000000000000000;hb=90993f1fc13288b83996066e726653b4dd011479;hpb=0f8b61908fb55c0ad2176708f2ea1775927bbd9b
diff --git a/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt b/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt
new file mode 100644
index 0000000..8722873
--- /dev/null
+++ b/src/main/kotlin/net/pterodactylus/sone/database/memory/MemoryDatabase.kt
@@ -0,0 +1,354 @@
+/*
+ * 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)
+ }
+ }
+
+}