✅ Fix failing memory database test
[Sone.git] / src / main / kotlin / net / pterodactylus / sone / database / memory / MemoryDatabase.kt
1 /*
2  * Sone - MemoryDatabase.kt - Copyright © 2013–2020 David Roden
3  *
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.
8  *
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.
13  *
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/>.
16  */
17
18 package net.pterodactylus.sone.database.memory
19
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
56
57 /**
58  * Memory-based [PostDatabase] implementation.
59  */
60 @Singleton
61 class MemoryDatabase constructor(private val configuration: Configuration, private val saveKnownPostRepliesRateLimiter: RateLimiter) : AbstractService(), Database {
62
63         @javax.inject.Inject constructor(configuration: Configuration): this(configuration, RateLimiter.create(1.0))
64
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)
84
85         override val soneLoader get() = this::getSone
86
87         override val sones get() = readLock.withLock { allSones.values.toSet() }
88
89         override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
90
91         override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
92
93         override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
94
95         override fun save() {
96                 if (saveRateLimiter.tryAcquire()) {
97                         saveKnownPosts()
98                         saveKnownPostReplies()
99                 }
100         }
101
102         override fun doStart() {
103                 memoryBookmarkDatabase.start()
104                 loadKnownPosts()
105                 loadKnownPostReplies()
106                 notifyStarted()
107         }
108
109         override fun doStop() {
110                 try {
111                         memoryBookmarkDatabase.stop()
112                         save()
113                         notifyStopped()
114                 } catch (de1: DatabaseException) {
115                         notifyFailed(de1)
116                 }
117         }
118
119         override fun newSoneBuilder() = MemorySoneBuilder(this)
120
121         override fun storeSone(sone: Sone) {
122                 writeLock.withLock {
123                         removeSone(sone)
124
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
129                         }
130                         sonePostReplies.putAll(sone.id, sone.replies)
131                         for (postReply in sone.replies) {
132                                 allPostReplies[postReply.id] = postReply.toShell()
133                         }
134                         sone.allAlbums.let { albums ->
135                                 soneAlbums.putAll(sone.id, albums)
136                                 albums.forEach { album -> allAlbums[album.id] = album }
137                         }
138                         sone.rootAlbum.allImages.let { images ->
139                                 soneImages.putAll(sone.id, images)
140                                 images.forEach { image -> allImages[image.id] = image }
141                         }
142                 }
143         }
144
145         override fun removeSone(sone: Sone) {
146                 writeLock.withLock {
147                         allSones.remove(sone.id)
148                         val removedPosts = sonePosts.removeAll(sone.id)
149                         for (removedPost in removedPosts) {
150                                 allPosts.remove(removedPost.id)
151                         }
152                         val removedPostReplies = sonePostReplies.removeAll(sone.id)
153                         for (removedPostReply in removedPostReplies) {
154                                 allPostReplies.remove(removedPostReply.id)
155                         }
156                         val removedAlbums = soneAlbums.removeAll(sone.id)
157                         for (removedAlbum in removedAlbums) {
158                                 allAlbums.remove(removedAlbum.id)
159                         }
160                         val removedImages = soneImages.removeAll(sone.id)
161                         for (removedImage in removedImages) {
162                                 allImages.remove(removedImage.id)
163                         }
164                 }
165         }
166
167         override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
168
169         override fun getFriends(localSone: Sone): Collection<String> =
170                         if (!localSone.isLocal) {
171                                 emptySet()
172                         } else {
173                                 memoryFriendDatabase.getFriends(localSone.id)
174                         }
175
176         override fun isFriend(localSone: Sone, friendSoneId: String) =
177                         if (!localSone.isLocal) {
178                                 false
179                         } else {
180                                 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
181                         }
182
183         override fun addFriend(localSone: Sone, friendSoneId: String) {
184                 if (!localSone.isLocal) {
185                         return
186                 }
187                 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
188         }
189
190         override fun removeFriend(localSone: Sone, friendSoneId: String) {
191                 if (!localSone.isLocal) {
192                         return
193                 }
194                 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
195         }
196
197         override fun getFollowingTime(friendSoneId: String) =
198                         memoryFriendDatabase.getFollowingTime(friendSoneId)
199
200         override fun getPost(postId: String): Post? =
201                         readLock.withLock { allPosts[postId]?.build(newPostBuilder()) }
202
203         override fun getPosts(soneId: String): Collection<Post> =
204                         sonePosts[soneId].map { it.build(newPostBuilder()) }.toSet()
205
206         override fun getDirectedPosts(recipientId: String) =
207                         readLock.withLock {
208                                 allPosts.values
209                                                 .filter { it.recipientId == recipientId }
210                                                 .map { it.build(newPostBuilder()) }
211                         }
212
213         override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
214
215         override fun storePost(post: Post) {
216                 checkNotNull(post, "post must not be null")
217                 writeLock.withLock {
218                         post.toShell().also { shell ->
219                                 allPosts[post.id] = shell
220                                 sonePosts[post.sone.id].add(shell)
221                         }
222                 }
223         }
224
225         override fun removePost(post: Post) {
226                 checkNotNull(post, "post must not be null")
227                 writeLock.withLock {
228                         allPosts.remove(post.id)
229                         sonePosts[post.sone.id].remove(post.toShell())
230                         post.sone.removePost(post)
231                 }
232         }
233
234         override fun getPostReply(id: String) = readLock.withLock {
235                 allPostReplies[id]?.build(newPostReplyBuilder())
236         }
237
238         override fun getReplies(postId: String) =
239                         readLock.withLock {
240                                 allPostReplies.values
241                                                 .filter { it.postId == postId }
242                                                 .map { it.build(newPostReplyBuilder()) }
243                                                 .sortedWith(newestReplyFirst.reversed())
244                         }
245
246         override fun newPostReplyBuilder(): PostReplyBuilder =
247                         MemoryPostReplyBuilder(this, this)
248
249         override fun storePostReply(postReply: PostReply) =
250                         writeLock.withLock {
251                                 allPostReplies[postReply.id] = postReply.toShell()
252                         }
253
254         override fun removePostReply(postReply: PostReply) =
255                         writeLock.withLock {
256                                 allPostReplies.remove(postReply.id)
257                         }.unit
258
259         override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
260
261         override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
262
263         override fun storeAlbum(album: Album) =
264                         writeLock.withLock {
265                                 allAlbums[album.id] = album
266                                 soneAlbums.put(album.sone.id, album)
267                         }.unit
268
269         override fun removeAlbum(album: Album) =
270                         writeLock.withLock {
271                                 allAlbums.remove(album.id)
272                                 soneAlbums.remove(album.sone.id, album)
273                         }.unit
274
275         override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
276
277         override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
278
279         override fun storeImage(image: Image): Unit =
280                         writeLock.withLock {
281                                 allImages[image.id] = image
282                                 soneImages.put(image.sone.id, image)
283                         }
284
285         override fun removeImage(image: Image): Unit =
286                         writeLock.withLock {
287                                 allImages.remove(image.id)
288                                 soneImages.remove(image.sone.id, image)
289                         }
290
291         override fun bookmarkPost(post: Post) =
292                         memoryBookmarkDatabase.bookmarkPost(post)
293
294         override fun unbookmarkPost(post: Post) =
295                         memoryBookmarkDatabase.unbookmarkPost(post)
296
297         override fun isPostBookmarked(post: Post) =
298                         memoryBookmarkDatabase.isPostBookmarked(post)
299
300         internal fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
301
302         fun setPostKnown(post: Post, known: Boolean): Unit =
303                         writeLock.withLock {
304                                 if (known)
305                                         knownPosts.add(post.id)
306                                 else
307                                         knownPosts.remove(post.id)
308                                 saveKnownPosts()
309                         }
310
311         internal fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
312
313         override fun setPostReplyKnown(postReply: PostReply): Unit =
314                         writeLock.withLock {
315                                 knownPostReplies.add(postReply.id)
316                                 saveKnownPostReplies()
317                         }
318
319         private fun loadKnownPosts() =
320                         configurationLoader.loadKnownPosts()
321                                         .let {
322                                                 writeLock.withLock {
323                                                         knownPosts.clear()
324                                                         knownPosts.addAll(it)
325                                                 }
326                                         }
327
328         private fun saveKnownPosts() =
329                         saveKnownPostsRateLimiter.tryAcquire().ifTrue {
330                                 try {
331                                         readLock.withLock {
332                                                 knownPosts.forEachIndexed { index, knownPostId ->
333                                                         configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
334                                                 }
335                                                 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
336                                         }
337                                 } catch (ce1: ConfigurationException) {
338                                         throw DatabaseException("Could not save database.", ce1)
339                                 }
340                         }
341
342         private fun loadKnownPostReplies(): Unit =
343                         configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
344                                 writeLock.withLock {
345                                         this.knownPostReplies.clear()
346                                         this.knownPostReplies.addAll(knownPostReplies)
347                                 }
348                         }
349
350         private fun saveKnownPostReplies() =
351                         saveKnownPostRepliesRateLimiter.tryAcquire().ifTrue {
352                                 try {
353                                         readLock.withLock {
354                                                 knownPostReplies.forEachIndexed { index, knownPostReply ->
355                                                         configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
356                                                 }
357                                                 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
358                                         }
359                                 } catch (ce1: ConfigurationException) {
360                                         throw DatabaseException("Could not save database.", ce1)
361                                 }
362                         }
363
364 }