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, MemoryPostReply.Shell>()
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.toShell()
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 {
227 allPostReplies[id]?.build(newPostReplyBuilder())
230 override fun getReplies(postId: String) =
232 allPostReplies.values
233 .filter { it.postId == postId }
234 .map { it.build(newPostReplyBuilder()) }
235 .sortedWith(newestReplyFirst.reversed())
238 override fun newPostReplyBuilder(): PostReplyBuilder =
239 MemoryPostReplyBuilder(this, this)
241 override fun storePostReply(postReply: PostReply) =
243 allPostReplies[postReply.id] = postReply.toShell()
246 override fun removePostReply(postReply: PostReply) =
248 allPostReplies.remove(postReply.id)
251 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
253 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
255 override fun storeAlbum(album: Album) =
257 allAlbums[album.id] = album
258 soneAlbums.put(album.sone.id, album)
261 override fun removeAlbum(album: Album) =
263 allAlbums.remove(album.id)
264 soneAlbums.remove(album.sone.id, album)
267 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
269 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
271 override fun storeImage(image: Image): Unit =
273 allImages[image.id] = image
274 soneImages.put(image.sone.id, image)
277 override fun removeImage(image: Image): Unit =
279 allImages.remove(image.id)
280 soneImages.remove(image.sone.id, image)
283 override fun bookmarkPost(post: Post) =
284 memoryBookmarkDatabase.bookmarkPost(post)
286 override fun unbookmarkPost(post: Post) =
287 memoryBookmarkDatabase.unbookmarkPost(post)
289 override fun isPostBookmarked(post: Post) =
290 memoryBookmarkDatabase.isPostBookmarked(post)
292 protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
294 fun setPostKnown(post: Post, known: Boolean): Unit =
297 knownPosts.add(post.id)
299 knownPosts.remove(post.id)
303 internal fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
305 override fun setPostReplyKnown(postReply: PostReply): Unit =
307 knownPostReplies.add(postReply.id)
308 saveKnownPostReplies()
311 private fun loadKnownPosts() =
312 configurationLoader.loadKnownPosts()
316 knownPosts.addAll(it)
320 private fun saveKnownPosts() =
321 saveKnownPostsRateLimiter.tryAcquire().ifTrue {
324 knownPosts.forEachIndexed { index, knownPostId ->
325 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
327 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
329 } catch (ce1: ConfigurationException) {
330 throw DatabaseException("Could not save database.", ce1)
334 private fun loadKnownPostReplies(): Unit =
335 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
337 this.knownPostReplies.clear()
338 this.knownPostReplies.addAll(knownPostReplies)
342 private fun saveKnownPostReplies() =
343 saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
346 knownPostReplies.forEachIndexed { index, knownPostReply ->
347 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
349 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
351 } catch (ce1: ConfigurationException) {
352 throw DatabaseException("Could not save database.", ce1)