2 * Sone - MemoryDatabase.kt - Copyright © 2013–2020 David Roden
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 package net.pterodactylus.sone.database.memory
20 import com.google.common.base.Preconditions.checkNotNull
21 import com.google.common.collect.HashMultimap
22 import com.google.common.collect.Multimap
23 import com.google.common.collect.TreeMultimap
24 import com.google.common.util.concurrent.AbstractService
25 import com.google.common.util.concurrent.RateLimiter
26 import com.google.inject.Inject
27 import com.google.inject.Singleton
28 import net.pterodactylus.sone.data.Album
29 import net.pterodactylus.sone.data.Image
30 import net.pterodactylus.sone.data.Post
31 import net.pterodactylus.sone.data.PostReply
32 import net.pterodactylus.sone.data.Sone
33 import net.pterodactylus.sone.data.allAlbums
34 import net.pterodactylus.sone.data.allImages
35 import net.pterodactylus.sone.data.impl.AlbumBuilderImpl
36 import net.pterodactylus.sone.data.impl.ImageBuilderImpl
37 import net.pterodactylus.sone.data.newestReplyFirst
38 import net.pterodactylus.sone.database.AlbumBuilder
39 import net.pterodactylus.sone.database.Database
40 import net.pterodactylus.sone.database.DatabaseException
41 import net.pterodactylus.sone.database.ImageBuilder
42 import net.pterodactylus.sone.database.PostBuilder
43 import net.pterodactylus.sone.database.PostDatabase
44 import net.pterodactylus.sone.database.PostReplyBuilder
45 import net.pterodactylus.sone.utils.ifTrue
46 import net.pterodactylus.sone.utils.unit
47 import net.pterodactylus.util.config.Configuration
48 import net.pterodactylus.util.config.ConfigurationException
49 import java.util.concurrent.locks.ReentrantReadWriteLock
50 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock
51 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock
52 import kotlin.concurrent.withLock
55 * Memory-based [PostDatabase] implementation.
58 class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
60 private val lock = ReentrantReadWriteLock()
61 private val readLock: ReadLock by lazy { lock.readLock() }
62 private val writeLock: WriteLock by lazy { lock.writeLock() }
63 private val configurationLoader = ConfigurationLoader(configuration)
64 private val allSones = mutableMapOf<String, Sone>()
65 private val allPosts = mutableMapOf<String, MemoryPost.Shell>()
66 private val sonePosts: Multimap<String, MemoryPost.Shell> = HashMultimap.create<String, MemoryPost.Shell>()
67 private val knownPosts = mutableSetOf<String>()
68 private val allPostReplies = mutableMapOf<String, MemoryPostReply.Shell>()
69 private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, newestReplyFirst)
70 private val knownPostReplies = mutableSetOf<String>()
71 private val allAlbums = mutableMapOf<String, Album>()
72 private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
73 private val allImages = mutableMapOf<String, Image>()
74 private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
75 private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader)
76 private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader)
77 private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0)
78 private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0)
79 private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0)
81 override val soneLoader get() = this::getSone
83 override val sones get() = readLock.withLock { allSones.values.toSet() }
85 override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
87 override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
89 override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
92 if (saveRateLimiter.tryAcquire()) {
94 saveKnownPostReplies()
98 override fun doStart() {
99 memoryBookmarkDatabase.start()
101 loadKnownPostReplies()
105 override fun doStop() {
107 memoryBookmarkDatabase.stop()
110 } catch (de1: DatabaseException) {
115 override fun newSoneBuilder() = MemorySoneBuilder(this)
117 override fun storeSone(sone: Sone) {
121 allSones[sone.id] = sone
122 sonePosts.putAll(sone.id, sone.posts.map(Post::toShell))
123 for (post in sone.posts.map(Post::toShell)) {
124 allPosts[post.id] = post
126 sonePostReplies.putAll(sone.id, sone.replies)
127 for (postReply in sone.replies) {
128 allPostReplies[postReply.id] = postReply.toShell()
130 sone.allAlbums.let { albums ->
131 soneAlbums.putAll(sone.id, albums)
132 albums.forEach { album -> allAlbums[album.id] = album }
134 sone.rootAlbum.allImages.let { images ->
135 soneImages.putAll(sone.id, images)
136 images.forEach { image -> allImages[image.id] = image }
141 override fun removeSone(sone: Sone) {
143 allSones.remove(sone.id)
144 val removedPosts = sonePosts.removeAll(sone.id)
145 for (removedPost in removedPosts) {
146 allPosts.remove(removedPost.id)
148 val removedPostReplies = sonePostReplies.removeAll(sone.id)
149 for (removedPostReply in removedPostReplies) {
150 allPostReplies.remove(removedPostReply.id)
152 val removedAlbums = soneAlbums.removeAll(sone.id)
153 for (removedAlbum in removedAlbums) {
154 allAlbums.remove(removedAlbum.id)
156 val removedImages = soneImages.removeAll(sone.id)
157 for (removedImage in removedImages) {
158 allImages.remove(removedImage.id)
163 override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
165 override fun getFriends(localSone: Sone): Collection<String> =
166 if (!localSone.isLocal) {
169 memoryFriendDatabase.getFriends(localSone.id)
172 override fun isFriend(localSone: Sone, friendSoneId: String) =
173 if (!localSone.isLocal) {
176 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
179 override fun addFriend(localSone: Sone, friendSoneId: String) {
180 if (!localSone.isLocal) {
183 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
186 override fun removeFriend(localSone: Sone, friendSoneId: String) {
187 if (!localSone.isLocal) {
190 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
193 override fun getFollowingTime(friendSoneId: String) =
194 memoryFriendDatabase.getFollowingTime(friendSoneId)
196 override fun getPost(postId: String): Post? =
197 readLock.withLock { allPosts[postId]?.build(newPostBuilder()) }
199 override fun getPosts(soneId: String): Collection<Post> =
200 sonePosts[soneId].map { it.build(newPostBuilder()) }.toSet()
202 override fun getDirectedPosts(recipientId: String) =
205 .filter { it.recipientId == recipientId }
206 .map { it.build(newPostBuilder()) }
209 override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
211 override fun storePost(post: Post) {
212 checkNotNull(post, "post must not be null")
214 post.toShell().also { shell ->
215 allPosts[post.id] = shell
216 sonePosts[post.sone.id].add(shell)
221 override fun removePost(post: Post) {
222 checkNotNull(post, "post must not be null")
224 allPosts.remove(post.id)
225 sonePosts[post.sone.id].remove(post.toShell())
226 post.sone.removePost(post)
230 override fun getPostReply(id: String) = readLock.withLock {
231 allPostReplies[id]?.build(newPostReplyBuilder())
234 override fun getReplies(postId: String) =
236 allPostReplies.values
237 .filter { it.postId == postId }
238 .map { it.build(newPostReplyBuilder()) }
239 .sortedWith(newestReplyFirst.reversed())
242 override fun newPostReplyBuilder(): PostReplyBuilder =
243 MemoryPostReplyBuilder(this, this)
245 override fun storePostReply(postReply: PostReply) =
247 allPostReplies[postReply.id] = postReply.toShell()
250 override fun removePostReply(postReply: PostReply) =
252 allPostReplies.remove(postReply.id)
255 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
257 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
259 override fun storeAlbum(album: Album) =
261 allAlbums[album.id] = album
262 soneAlbums.put(album.sone.id, album)
265 override fun removeAlbum(album: Album) =
267 allAlbums.remove(album.id)
268 soneAlbums.remove(album.sone.id, album)
271 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
273 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
275 override fun storeImage(image: Image): Unit =
277 allImages[image.id] = image
278 soneImages.put(image.sone.id, image)
281 override fun removeImage(image: Image): Unit =
283 allImages.remove(image.id)
284 soneImages.remove(image.sone.id, image)
287 override fun bookmarkPost(post: Post) =
288 memoryBookmarkDatabase.bookmarkPost(post)
290 override fun unbookmarkPost(post: Post) =
291 memoryBookmarkDatabase.unbookmarkPost(post)
293 override fun isPostBookmarked(post: Post) =
294 memoryBookmarkDatabase.isPostBookmarked(post)
296 internal fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
298 fun setPostKnown(post: Post, known: Boolean): Unit =
301 knownPosts.add(post.id)
303 knownPosts.remove(post.id)
307 internal fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
309 override fun setPostReplyKnown(postReply: PostReply): Unit =
311 knownPostReplies.add(postReply.id)
312 saveKnownPostReplies()
315 private fun loadKnownPosts() =
316 configurationLoader.loadKnownPosts()
320 knownPosts.addAll(it)
324 private fun saveKnownPosts() =
325 saveKnownPostsRateLimiter.tryAcquire().ifTrue {
328 knownPosts.forEachIndexed { index, knownPostId ->
329 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
331 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
333 } catch (ce1: ConfigurationException) {
334 throw DatabaseException("Could not save database.", ce1)
338 private fun loadKnownPostReplies(): Unit =
339 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
341 this.knownPostReplies.clear()
342 this.knownPostReplies.addAll(knownPostReplies)
346 private fun saveKnownPostReplies() =
347 saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
350 knownPostReplies.forEachIndexed { index, knownPostReply ->
351 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
353 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
355 } catch (ce1: ConfigurationException) {
356 throw DatabaseException("Could not save database.", ce1)