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.*
25 import com.google.inject.Inject
26 import com.google.inject.Singleton
27 import net.pterodactylus.sone.data.Album
28 import net.pterodactylus.sone.data.Image
29 import net.pterodactylus.sone.data.Post
30 import net.pterodactylus.sone.data.PostReply
31 import net.pterodactylus.sone.data.Reply.TIME_COMPARATOR
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.database.AlbumBuilder
38 import net.pterodactylus.sone.database.Database
39 import net.pterodactylus.sone.database.DatabaseException
40 import net.pterodactylus.sone.database.ImageBuilder
41 import net.pterodactylus.sone.database.PostBuilder
42 import net.pterodactylus.sone.database.PostDatabase
43 import net.pterodactylus.sone.database.PostReplyBuilder
44 import net.pterodactylus.sone.utils.*
45 import net.pterodactylus.util.config.Configuration
46 import net.pterodactylus.util.config.ConfigurationException
47 import java.util.concurrent.locks.ReentrantReadWriteLock
48 import kotlin.concurrent.withLock
51 * Memory-based [PostDatabase] implementation.
54 class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
56 private val lock = ReentrantReadWriteLock()
57 private val readLock by lazy { lock.readLock()!! }
58 private val writeLock by lazy { lock.writeLock()!! }
59 private val configurationLoader = ConfigurationLoader(configuration)
60 private val allSones = mutableMapOf<String, Sone>()
61 private val allPosts = mutableMapOf<String, Post>()
62 private val sonePosts: Multimap<String, Post> = HashMultimap.create<String, Post>()
63 private val knownPosts = mutableSetOf<String>()
64 private val allPostReplies = mutableMapOf<String, PostReply>()
65 private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, TIME_COMPARATOR)
66 private val knownPostReplies = mutableSetOf<String>()
67 private val allAlbums = mutableMapOf<String, Album>()
68 private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
69 private val allImages = mutableMapOf<String, Image>()
70 private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
71 private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader)
72 private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader)
73 private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0)
74 private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0)
75 private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0)
77 override val soneLoader get() = this::getSone
79 override val sones get() = readLock.withLock { allSones.values.toSet() }
81 override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
83 override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
85 override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
88 if (saveRateLimiter.tryAcquire()) {
90 saveKnownPostReplies()
94 override fun doStart() {
95 memoryBookmarkDatabase.start()
97 loadKnownPostReplies()
101 override fun doStop() {
103 memoryBookmarkDatabase.stop()
106 } catch (de1: DatabaseException) {
111 override fun newSoneBuilder() = MemorySoneBuilder(this)
113 override fun storeSone(sone: Sone) {
117 allSones[sone.id] = sone
118 sonePosts.putAll(sone.id, sone.posts)
119 for (post in sone.posts) {
120 allPosts[post.id] = post
122 sonePostReplies.putAll(sone.id, sone.replies)
123 for (postReply in sone.replies) {
124 allPostReplies[postReply.id] = postReply
126 sone.allAlbums.let { albums ->
127 soneAlbums.putAll(sone.id, albums)
128 albums.forEach { album -> allAlbums[album.id] = album }
130 sone.rootAlbum.allImages.let { images ->
131 soneImages.putAll(sone.id, images)
132 images.forEach { image -> allImages[image.id] = image }
137 override fun removeSone(sone: Sone) {
139 allSones.remove(sone.id)
140 val removedPosts = sonePosts.removeAll(sone.id)
141 for (removedPost in removedPosts) {
142 allPosts.remove(removedPost.id)
144 val removedPostReplies = sonePostReplies.removeAll(sone.id)
145 for (removedPostReply in removedPostReplies) {
146 allPostReplies.remove(removedPostReply.id)
148 val removedAlbums = soneAlbums.removeAll(sone.id)
149 for (removedAlbum in removedAlbums) {
150 allAlbums.remove(removedAlbum.id)
152 val removedImages = soneImages.removeAll(sone.id)
153 for (removedImage in removedImages) {
154 allImages.remove(removedImage.id)
159 override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
161 override fun getFriends(localSone: Sone): Collection<String> =
162 if (!localSone.isLocal) {
165 memoryFriendDatabase.getFriends(localSone.id)
168 override fun isFriend(localSone: Sone, friendSoneId: String) =
169 if (!localSone.isLocal) {
172 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
175 override fun addFriend(localSone: Sone, friendSoneId: String) {
176 if (!localSone.isLocal) {
179 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
182 override fun removeFriend(localSone: Sone, friendSoneId: String) {
183 if (!localSone.isLocal) {
186 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
189 override fun getFollowingTime(friendSoneId: String) =
190 memoryFriendDatabase.getFollowingTime(friendSoneId)
192 override fun getPost(postId: String) =
193 readLock.withLock { allPosts[postId] }
195 override fun getPosts(soneId: String): Collection<Post> =
196 sonePosts[soneId].toSet()
198 override fun getDirectedPosts(recipientId: String) =
200 allPosts.values.filter {
201 it.recipientId.orNull() == recipientId
205 override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
207 override fun storePost(post: Post) {
208 checkNotNull(post, "post must not be null")
210 allPosts[post.id] = post
211 sonePosts[post.sone.id].add(post)
215 override fun removePost(post: Post) {
216 checkNotNull(post, "post must not be null")
218 allPosts.remove(post.id)
219 sonePosts[post.sone.id].remove(post)
220 post.sone.removePost(post)
224 override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
226 override fun getReplies(postId: String) =
228 allPostReplies.values
229 .filter { it.postId == postId }
230 .sortedWith(TIME_COMPARATOR)
233 override fun newPostReplyBuilder(): PostReplyBuilder =
234 MemoryPostReplyBuilder(this, this)
236 override fun storePostReply(postReply: PostReply) =
238 allPostReplies[postReply.id] = postReply
241 override fun removePostReply(postReply: PostReply) =
243 allPostReplies.remove(postReply.id)
246 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
248 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
250 override fun storeAlbum(album: Album) =
252 allAlbums[album.id] = album
253 soneAlbums.put(album.sone.id, album)
256 override fun removeAlbum(album: Album) =
258 allAlbums.remove(album.id)
259 soneAlbums.remove(album.sone.id, album)
262 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
264 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
266 override fun storeImage(image: Image): Unit =
268 allImages[image.id] = image
269 soneImages.put(image.sone.id, image)
272 override fun removeImage(image: Image): Unit =
274 allImages.remove(image.id)
275 soneImages.remove(image.sone.id, image)
278 override fun bookmarkPost(post: Post) =
279 memoryBookmarkDatabase.bookmarkPost(post)
281 override fun unbookmarkPost(post: Post) =
282 memoryBookmarkDatabase.unbookmarkPost(post)
284 override fun isPostBookmarked(post: Post) =
285 memoryBookmarkDatabase.isPostBookmarked(post)
287 protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
289 fun setPostKnown(post: Post, known: Boolean): Unit =
292 knownPosts.add(post.id)
294 knownPosts.remove(post.id)
298 protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
300 fun setPostReplyKnown(postReply: PostReply, known: Boolean): Unit =
303 knownPostReplies.add(postReply.id)
305 knownPostReplies.remove(postReply.id)
306 saveKnownPostReplies()
309 private fun loadKnownPosts() =
310 configurationLoader.loadKnownPosts()
314 knownPosts.addAll(it)
318 private fun saveKnownPosts() =
319 saveKnownPostsRateLimiter.tryAcquire().ifTrue {
322 knownPosts.forEachIndexed { index, knownPostId ->
323 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
325 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
327 } catch (ce1: ConfigurationException) {
328 throw DatabaseException("Could not save database.", ce1)
332 private fun loadKnownPostReplies(): Unit =
333 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
335 this.knownPostReplies.clear()
336 this.knownPostReplies.addAll(knownPostReplies)
340 private fun saveKnownPostReplies() =
341 saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
344 knownPostReplies.forEachIndexed { index, knownPostReply ->
345 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
347 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
349 } catch (ce1: ConfigurationException) {
350 throw DatabaseException("Could not save database.", ce1)