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