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