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