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.PostReplyShell
33 import net.pterodactylus.sone.data.PostShell
34 import net.pterodactylus.sone.data.Sone
35 import net.pterodactylus.sone.data.allAlbums
36 import net.pterodactylus.sone.data.allImages
37 import net.pterodactylus.sone.data.impl.AlbumBuilderImpl
38 import net.pterodactylus.sone.data.impl.ImageBuilderImpl
39 import net.pterodactylus.sone.data.newestReplyFirst
40 import net.pterodactylus.sone.data.toShell
41 import net.pterodactylus.sone.database.AlbumBuilder
42 import net.pterodactylus.sone.database.Database
43 import net.pterodactylus.sone.database.DatabaseException
44 import net.pterodactylus.sone.database.ImageBuilder
45 import net.pterodactylus.sone.database.PostBuilder
46 import net.pterodactylus.sone.database.PostDatabase
47 import net.pterodactylus.sone.database.PostReplyBuilder
48 import net.pterodactylus.sone.utils.ifTrue
49 import net.pterodactylus.sone.utils.unit
50 import net.pterodactylus.util.config.Configuration
51 import net.pterodactylus.util.config.ConfigurationException
52 import java.util.concurrent.locks.ReentrantReadWriteLock
53 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock
54 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock
55 import kotlin.concurrent.withLock
58 * Memory-based [PostDatabase] implementation.
61 class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
63 private val lock = ReentrantReadWriteLock()
64 private val readLock: ReadLock by lazy { lock.readLock() }
65 private val writeLock: WriteLock by lazy { lock.writeLock() }
66 private val configurationLoader = ConfigurationLoader(configuration)
67 private val allSones = mutableMapOf<String, Sone>()
68 private val allPosts = mutableMapOf<String, PostShell>()
69 private val sonePosts: Multimap<String, PostShell> = HashMultimap.create<String, PostShell>()
70 private val knownPosts = mutableSetOf<String>()
71 private val allPostReplies = mutableMapOf<String, PostReplyShell>()
72 private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, newestReplyFirst)
73 private val knownPostReplies = mutableSetOf<String>()
74 private val allAlbums = mutableMapOf<String, Album>()
75 private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
76 private val allImages = mutableMapOf<String, Image>()
77 private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
78 private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader)
79 private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader)
80 private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0)
81 private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0)
82 private val saveKnownPostRepliesRateLimiter: RateLimiter = RateLimiter.create(1.0)
84 override val soneLoader get() = this::getSone
86 override val sones get() = readLock.withLock { allSones.values.toSet() }
88 override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
90 override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
92 override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
95 if (saveRateLimiter.tryAcquire()) {
97 saveKnownPostReplies()
101 override fun doStart() {
102 memoryBookmarkDatabase.start()
104 loadKnownPostReplies()
108 override fun doStop() {
110 memoryBookmarkDatabase.stop()
113 } catch (de1: DatabaseException) {
118 override fun newSoneBuilder() = MemorySoneBuilder(this)
120 override fun storeSone(sone: Sone) {
124 allSones[sone.id] = sone
125 sonePosts.putAll(sone.id, sone.posts.map(Post::toShell))
126 for (post in sone.posts.map(Post::toShell)) {
127 allPosts[post.id] = post
129 sonePostReplies.putAll(sone.id, sone.replies)
130 for (postReply in sone.replies) {
131 allPostReplies[postReply.id] = postReply.toShell()
133 sone.allAlbums.let { albums ->
134 soneAlbums.putAll(sone.id, albums)
135 albums.forEach { album -> allAlbums[album.id] = album }
137 sone.rootAlbum.allImages.let { images ->
138 soneImages.putAll(sone.id, images)
139 images.forEach { image -> allImages[image.id] = image }
144 override fun removeSone(sone: Sone) {
146 allSones.remove(sone.id)
147 val removedPosts = sonePosts.removeAll(sone.id)
148 for (removedPost in removedPosts) {
149 allPosts.remove(removedPost.id)
151 val removedPostReplies = sonePostReplies.removeAll(sone.id)
152 for (removedPostReply in removedPostReplies) {
153 allPostReplies.remove(removedPostReply.id)
155 val removedAlbums = soneAlbums.removeAll(sone.id)
156 for (removedAlbum in removedAlbums) {
157 allAlbums.remove(removedAlbum.id)
159 val removedImages = soneImages.removeAll(sone.id)
160 for (removedImage in removedImages) {
161 allImages.remove(removedImage.id)
166 override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
168 override fun getFriends(localSone: Sone): Collection<String> =
169 if (!localSone.isLocal) {
172 memoryFriendDatabase.getFriends(localSone.id)
175 override fun isFriend(localSone: Sone, friendSoneId: String) =
176 if (!localSone.isLocal) {
179 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
182 override fun addFriend(localSone: Sone, friendSoneId: String) {
183 if (!localSone.isLocal) {
186 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
189 override fun removeFriend(localSone: Sone, friendSoneId: String) {
190 if (!localSone.isLocal) {
193 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
196 override fun getFollowingTime(friendSoneId: String) =
197 memoryFriendDatabase.getFollowingTime(friendSoneId)
199 override fun getPost(postId: String): Post? =
200 readLock.withLock { allPosts[postId]?.build(newPostBuilder()) }
202 override fun getPosts(soneId: String): Collection<Post> =
203 sonePosts[soneId].map { it.build(newPostBuilder()) }.toSet()
205 override fun getDirectedPosts(recipientId: String) =
208 .filter { it.recipientId == recipientId }
209 .map { it.build(newPostBuilder()) }
212 override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
214 override fun storePost(post: Post) {
215 checkNotNull(post, "post must not be null")
217 post.toShell().also { shell ->
218 allPosts[post.id] = shell
219 sonePosts[post.sone.id].add(shell)
224 override fun removePost(post: Post) {
225 checkNotNull(post, "post must not be null")
227 allPosts.remove(post.id)
228 sonePosts[post.sone.id].remove(post.toShell())
229 post.sone.removePost(post)
233 override fun getPostReply(id: String) = readLock.withLock {
234 allPostReplies[id]?.build(newPostReplyBuilder())
237 override fun getReplies(postId: String) =
239 allPostReplies.values
240 .filter { it.postId == postId }
241 .map { it.build(newPostReplyBuilder()) }
242 .sortedWith(newestReplyFirst.reversed())
245 override fun newPostReplyBuilder(): PostReplyBuilder =
246 MemoryPostReplyBuilder(this, this)
248 override fun storePostReply(postReply: PostReply) =
250 allPostReplies[postReply.id] = postReply.toShell()
253 override fun removePostReply(postReply: PostReply) =
255 allPostReplies.remove(postReply.id)
258 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
260 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
262 override fun storeAlbum(album: Album) =
264 allAlbums[album.id] = album
265 soneAlbums.put(album.sone.id, album)
268 override fun removeAlbum(album: Album) =
270 allAlbums.remove(album.id)
271 soneAlbums.remove(album.sone.id, album)
274 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
276 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
278 override fun storeImage(image: Image): Unit =
280 allImages[image.id] = image
281 soneImages.put(image.sone.id, image)
284 override fun removeImage(image: Image): Unit =
286 allImages.remove(image.id)
287 soneImages.remove(image.sone.id, image)
290 override fun bookmarkPost(post: Post) =
291 memoryBookmarkDatabase.bookmarkPost(post)
293 override fun unbookmarkPost(post: Post) =
294 memoryBookmarkDatabase.unbookmarkPost(post)
296 override fun isPostBookmarked(post: Post) =
297 memoryBookmarkDatabase.isPostBookmarked(post)
299 internal fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
301 fun setPostKnown(post: Post, known: Boolean): Unit =
304 knownPosts.add(post.id)
306 knownPosts.remove(post.id)
310 internal fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
312 override fun setPostReplyKnown(postReply: PostReply): Unit =
314 knownPostReplies.add(postReply.id)
315 saveKnownPostReplies()
318 private fun loadKnownPosts() =
319 configurationLoader.loadKnownPosts()
323 knownPosts.addAll(it)
327 private fun saveKnownPosts() =
328 saveKnownPostsRateLimiter.tryAcquire().ifTrue {
331 knownPosts.forEachIndexed { index, knownPostId ->
332 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
334 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
336 } catch (ce1: ConfigurationException) {
337 throw DatabaseException("Could not save database.", ce1)
341 private fun loadKnownPostReplies(): Unit =
342 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
344 this.knownPostReplies.clear()
345 this.knownPostReplies.addAll(knownPostReplies)
349 private fun saveKnownPostReplies() =
350 saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
353 knownPostReplies.forEachIndexed { index, knownPostReply ->
354 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
356 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
358 } catch (ce1: ConfigurationException) {
359 throw DatabaseException("Could not save database.", ce1)