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