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 kotlin.concurrent.withLock
53 * Memory-based [PostDatabase] implementation.
56 class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
58 private val lock = ReentrantReadWriteLock()
59 private val readLock by lazy { lock.readLock()!! }
60 private val writeLock by lazy { lock.writeLock()!! }
61 private val configurationLoader = ConfigurationLoader(configuration)
62 private val allSones = mutableMapOf<String, Sone>()
63 private val allPosts = mutableMapOf<String, Post>()
64 private val sonePosts: Multimap<String, Post> = HashMultimap.create<String, Post>()
65 private val knownPosts = mutableSetOf<String>()
66 private val allPostReplies = mutableMapOf<String, PostReply>()
67 private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, newestReplyFirst)
68 private val knownPostReplies = mutableSetOf<String>()
69 private val allAlbums = mutableMapOf<String, Album>()
70 private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
71 private val allImages = mutableMapOf<String, Image>()
72 private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
73 private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader)
74 private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader)
75 private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0)
76 private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0)
77 private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0)
79 override val soneLoader get() = this::getSone
81 override val sones get() = readLock.withLock { allSones.values.toSet() }
83 override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
85 override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
87 override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
90 if (saveRateLimiter.tryAcquire()) {
92 saveKnownPostReplies()
96 override fun doStart() {
97 memoryBookmarkDatabase.start()
99 loadKnownPostReplies()
103 override fun doStop() {
105 memoryBookmarkDatabase.stop()
108 } catch (de1: DatabaseException) {
113 override fun newSoneBuilder() = MemorySoneBuilder(this)
115 override fun storeSone(sone: Sone) {
119 allSones[sone.id] = sone
120 sonePosts.putAll(sone.id, sone.posts)
121 for (post in sone.posts) {
122 allPosts[post.id] = post
124 sonePostReplies.putAll(sone.id, sone.replies)
125 for (postReply in sone.replies) {
126 allPostReplies[postReply.id] = postReply
128 sone.allAlbums.let { albums ->
129 soneAlbums.putAll(sone.id, albums)
130 albums.forEach { album -> allAlbums[album.id] = album }
132 sone.rootAlbum.allImages.let { images ->
133 soneImages.putAll(sone.id, images)
134 images.forEach { image -> allImages[image.id] = image }
139 override fun removeSone(sone: Sone) {
141 allSones.remove(sone.id)
142 val removedPosts = sonePosts.removeAll(sone.id)
143 for (removedPost in removedPosts) {
144 allPosts.remove(removedPost.id)
146 val removedPostReplies = sonePostReplies.removeAll(sone.id)
147 for (removedPostReply in removedPostReplies) {
148 allPostReplies.remove(removedPostReply.id)
150 val removedAlbums = soneAlbums.removeAll(sone.id)
151 for (removedAlbum in removedAlbums) {
152 allAlbums.remove(removedAlbum.id)
154 val removedImages = soneImages.removeAll(sone.id)
155 for (removedImage in removedImages) {
156 allImages.remove(removedImage.id)
161 override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
163 override fun getFriends(localSone: Sone): Collection<String> =
164 if (!localSone.isLocal) {
167 memoryFriendDatabase.getFriends(localSone.id)
170 override fun isFriend(localSone: Sone, friendSoneId: String) =
171 if (!localSone.isLocal) {
174 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
177 override fun addFriend(localSone: Sone, friendSoneId: String) {
178 if (!localSone.isLocal) {
181 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
184 override fun removeFriend(localSone: Sone, friendSoneId: String) {
185 if (!localSone.isLocal) {
188 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
191 override fun getFollowingTime(friendSoneId: String) =
192 memoryFriendDatabase.getFollowingTime(friendSoneId)
194 override fun getPost(postId: String) =
195 readLock.withLock { allPosts[postId] }
197 override fun getPosts(soneId: String): Collection<Post> =
198 sonePosts[soneId].toSet()
200 override fun getDirectedPosts(recipientId: String) =
202 allPosts.values.filter {
203 it.recipientId.orNull() == recipientId
207 override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
209 override fun storePost(post: Post) {
210 checkNotNull(post, "post must not be null")
212 allPosts[post.id] = post
213 sonePosts[post.sone.id].add(post)
217 override fun removePost(post: Post) {
218 checkNotNull(post, "post must not be null")
220 allPosts.remove(post.id)
221 sonePosts[post.sone.id].remove(post)
222 post.sone.removePost(post)
226 override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
228 override fun getReplies(postId: String) =
230 allPostReplies.values
231 .filter { it.postId == postId }
232 .sortedWith(newestReplyFirst.reversed())
235 override fun newPostReplyBuilder(): PostReplyBuilder =
236 MemoryPostReplyBuilder(this, this)
238 override fun storePostReply(postReply: PostReply) =
240 allPostReplies[postReply.id] = postReply
243 override fun removePostReply(postReply: PostReply) =
245 allPostReplies.remove(postReply.id)
248 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
250 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
252 override fun storeAlbum(album: Album) =
254 allAlbums[album.id] = album
255 soneAlbums.put(album.sone.id, album)
258 override fun removeAlbum(album: Album) =
260 allAlbums.remove(album.id)
261 soneAlbums.remove(album.sone.id, album)
264 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
266 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
268 override fun storeImage(image: Image): Unit =
270 allImages[image.id] = image
271 soneImages.put(image.sone.id, image)
274 override fun removeImage(image: Image): Unit =
276 allImages.remove(image.id)
277 soneImages.remove(image.sone.id, image)
280 override fun bookmarkPost(post: Post) =
281 memoryBookmarkDatabase.bookmarkPost(post)
283 override fun unbookmarkPost(post: Post) =
284 memoryBookmarkDatabase.unbookmarkPost(post)
286 override fun isPostBookmarked(post: Post) =
287 memoryBookmarkDatabase.isPostBookmarked(post)
289 protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
291 fun setPostKnown(post: Post, known: Boolean): Unit =
294 knownPosts.add(post.id)
296 knownPosts.remove(post.id)
300 protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
302 override fun setPostReplyKnown(postReply: PostReply): Unit =
304 knownPostReplies.add(postReply.id)
305 saveKnownPostReplies()
308 private fun loadKnownPosts() =
309 configurationLoader.loadKnownPosts()
313 knownPosts.addAll(it)
317 private fun saveKnownPosts() =
318 saveKnownPostsRateLimiter.tryAcquire().ifTrue {
321 knownPosts.forEachIndexed { index, knownPostId ->
322 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
324 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
326 } catch (ce1: ConfigurationException) {
327 throw DatabaseException("Could not save database.", ce1)
331 private fun loadKnownPostReplies(): Unit =
332 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
334 this.knownPostReplies.clear()
335 this.knownPostReplies.addAll(knownPostReplies)
339 private fun saveKnownPostReplies() =
340 saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
343 knownPostReplies.forEachIndexed { index, knownPostReply ->
344 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
346 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
348 } catch (ce1: ConfigurationException) {
349 throw DatabaseException("Could not save database.", ce1)