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 constructor(private val configuration: Configuration, private val saveKnownPostRepliesRateLimiter: RateLimiter) : AbstractService(), Database {
63 @javax.inject.Inject constructor(configuration: Configuration): this(configuration, RateLimiter.create(1.0))
65 private val lock = ReentrantReadWriteLock()
66 private val readLock: ReadLock by lazy { lock.readLock() }
67 private val writeLock: WriteLock by lazy { lock.writeLock() }
68 private val configurationLoader = ConfigurationLoader(configuration)
69 private val allSones = mutableMapOf<String, Sone>()
70 private val allPosts = mutableMapOf<String, PostShell>()
71 private val sonePosts: Multimap<String, PostShell> = HashMultimap.create<String, PostShell>()
72 private val knownPosts = mutableSetOf<String>()
73 private val allPostReplies = mutableMapOf<String, PostReplyShell>()
74 private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, newestReplyFirst)
75 private val knownPostReplies = mutableSetOf<String>()
76 private val allAlbums = mutableMapOf<String, Album>()
77 private val soneAlbums: Multimap<String, Album> = HashMultimap.create<String, Album>()
78 private val allImages = mutableMapOf<String, Image>()
79 private val soneImages: Multimap<String, Image> = HashMultimap.create<String, Image>()
80 private val memoryBookmarkDatabase = MemoryBookmarkDatabase(this, configurationLoader)
81 private val memoryFriendDatabase = MemoryFriendDatabase(configurationLoader)
82 private val saveRateLimiter: RateLimiter = RateLimiter.create(1.0)
83 private val saveKnownPostsRateLimiter: RateLimiter = RateLimiter.create(1.0)
85 override val soneLoader get() = this::getSone
87 override val sones get() = readLock.withLock { allSones.values.toSet() }
89 override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
91 override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
93 override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
96 if (saveRateLimiter.tryAcquire()) {
98 saveKnownPostReplies()
102 override fun doStart() {
103 memoryBookmarkDatabase.start()
105 loadKnownPostReplies()
109 override fun doStop() {
111 memoryBookmarkDatabase.stop()
114 } catch (de1: DatabaseException) {
119 override fun newSoneBuilder() = MemorySoneBuilder(this)
121 override fun storeSone(sone: Sone) {
125 allSones[sone.id] = sone
126 sonePosts.putAll(sone.id, sone.posts.map(Post::toShell))
127 for (post in sone.posts.map(Post::toShell)) {
128 allPosts[post.id] = post
130 sonePostReplies.putAll(sone.id, sone.replies)
131 for (postReply in sone.replies) {
132 allPostReplies[postReply.id] = postReply.toShell()
134 sone.allAlbums.let { albums ->
135 soneAlbums.putAll(sone.id, albums)
136 albums.forEach { album -> allAlbums[album.id] = album }
138 sone.rootAlbum.allImages.let { images ->
139 soneImages.putAll(sone.id, images)
140 images.forEach { image -> allImages[image.id] = image }
145 override fun removeSone(sone: Sone) {
147 allSones.remove(sone.id)
148 val removedPosts = sonePosts.removeAll(sone.id)
149 for (removedPost in removedPosts) {
150 allPosts.remove(removedPost.id)
152 val removedPostReplies = sonePostReplies.removeAll(sone.id)
153 for (removedPostReply in removedPostReplies) {
154 allPostReplies.remove(removedPostReply.id)
156 val removedAlbums = soneAlbums.removeAll(sone.id)
157 for (removedAlbum in removedAlbums) {
158 allAlbums.remove(removedAlbum.id)
160 val removedImages = soneImages.removeAll(sone.id)
161 for (removedImage in removedImages) {
162 allImages.remove(removedImage.id)
167 override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
169 override fun getFriends(localSone: Sone): Collection<String> =
170 if (!localSone.isLocal) {
173 memoryFriendDatabase.getFriends(localSone.id)
176 override fun isFriend(localSone: Sone, friendSoneId: String) =
177 if (!localSone.isLocal) {
180 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
183 override fun addFriend(localSone: Sone, friendSoneId: String) {
184 if (!localSone.isLocal) {
187 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
190 override fun removeFriend(localSone: Sone, friendSoneId: String) {
191 if (!localSone.isLocal) {
194 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
197 override fun getFollowingTime(friendSoneId: String) =
198 memoryFriendDatabase.getFollowingTime(friendSoneId)
200 override fun getPost(postId: String): Post? =
201 readLock.withLock { allPosts[postId]?.build(newPostBuilder()) }
203 override fun getPosts(soneId: String): Collection<Post> =
204 sonePosts[soneId].map { it.build(newPostBuilder()) }.toSet()
206 override fun getDirectedPosts(recipientId: String) =
209 .filter { it.recipientId == recipientId }
210 .map { it.build(newPostBuilder()) }
213 override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
215 override fun storePost(post: Post) {
216 checkNotNull(post, "post must not be null")
218 post.toShell().also { shell ->
219 allPosts[post.id] = shell
220 sonePosts[post.sone.id].add(shell)
225 override fun removePost(post: Post) {
226 checkNotNull(post, "post must not be null")
228 allPosts.remove(post.id)
229 sonePosts[post.sone.id].remove(post.toShell())
230 post.sone.removePost(post)
234 override fun getPostReply(id: String) = readLock.withLock {
235 allPostReplies[id]?.build(newPostReplyBuilder())
238 override fun getReplies(postId: String) =
240 allPostReplies.values
241 .filter { it.postId == postId }
242 .map { it.build(newPostReplyBuilder()) }
243 .sortedWith(newestReplyFirst.reversed())
246 override fun newPostReplyBuilder(): PostReplyBuilder =
247 MemoryPostReplyBuilder(this, this)
249 override fun storePostReply(postReply: PostReply) =
251 allPostReplies[postReply.id] = postReply.toShell()
254 override fun removePostReply(postReply: PostReply) =
256 allPostReplies.remove(postReply.id)
259 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
261 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
263 override fun storeAlbum(album: Album) =
265 allAlbums[album.id] = album
266 soneAlbums.put(album.sone.id, album)
269 override fun removeAlbum(album: Album) =
271 allAlbums.remove(album.id)
272 soneAlbums.remove(album.sone.id, album)
275 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
277 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
279 override fun storeImage(image: Image): Unit =
281 allImages[image.id] = image
282 soneImages.put(image.sone.id, image)
285 override fun removeImage(image: Image): Unit =
287 allImages.remove(image.id)
288 soneImages.remove(image.sone.id, image)
291 override fun bookmarkPost(post: Post) =
292 memoryBookmarkDatabase.bookmarkPost(post)
294 override fun unbookmarkPost(post: Post) =
295 memoryBookmarkDatabase.unbookmarkPost(post)
297 override fun isPostBookmarked(post: Post) =
298 memoryBookmarkDatabase.isPostBookmarked(post)
300 internal fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
302 fun setPostKnown(post: Post, known: Boolean): Unit =
305 knownPosts.add(post.id)
307 knownPosts.remove(post.id)
311 internal fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
313 override fun setPostReplyKnown(postReply: PostReply): Unit =
315 knownPostReplies.add(postReply.id)
316 saveKnownPostReplies()
319 private fun loadKnownPosts() =
320 configurationLoader.loadKnownPosts()
324 knownPosts.addAll(it)
328 private fun saveKnownPosts() =
329 saveKnownPostsRateLimiter.tryAcquire().ifTrue {
332 knownPosts.forEachIndexed { index, knownPostId ->
333 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
335 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
337 } catch (ce1: ConfigurationException) {
338 throw DatabaseException("Could not save database.", ce1)
342 private fun loadKnownPostReplies(): Unit =
343 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
345 this.knownPostReplies.clear()
346 this.knownPostReplies.addAll(knownPostReplies)
350 private fun saveKnownPostReplies() =
351 saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
354 knownPostReplies.forEachIndexed { index, knownPostReply ->
355 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
357 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
359 } catch (ce1: ConfigurationException) {
360 throw DatabaseException("Could not save database.", ce1)