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