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