702d4e210951241688ace6ccff73f16c3f8551df
[Sone.git] / src / main / java / net / pterodactylus / sone / database / memory / MemoryDatabase.kt
1 /*
2  * Sone - MemoryDatabase.java - Copyright © 2013–2016 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  * @author [David ‘Bombe’ Roden](mailto:bombe@pterodactylus.net)
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, PostReply>()
67         private val sonePostReplies: Multimap<String, PostReply> = TreeMultimap.create<String, PostReply>(Comparator { leftString, rightString -> leftString.compareTo(rightString) }, TIME_COMPARATOR)
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
76         override val soneLoader get() = this::getSone
77
78         override val sones get() = readLock.withLock { allSones.values.toSet() }
79
80         override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
81
82         override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
83
84         override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
85
86         override fun save() {
87                 saveKnownPosts()
88                 saveKnownPostReplies()
89         }
90
91         override fun doStart() {
92                 memoryBookmarkDatabase.start()
93                 loadKnownPosts()
94                 loadKnownPostReplies()
95                 notifyStarted()
96         }
97
98         override fun doStop() {
99                 try {
100                         memoryBookmarkDatabase.stop()
101                         save()
102                         notifyStopped()
103                 } catch (de1: DatabaseException) {
104                         notifyFailed(de1)
105                 }
106         }
107
108         override fun newSoneBuilder() = MemorySoneBuilder(this)
109
110         override fun storeSone(sone: Sone) {
111                 writeLock.withLock {
112                         removeSone(sone)
113
114                         allSones[sone.id] = sone
115                         sonePosts.putAll(sone.id, sone.posts)
116                         for (post in sone.posts) {
117                                 allPosts[post.id] = post
118                         }
119                         sonePostReplies.putAll(sone.id, sone.replies)
120                         for (postReply in sone.replies) {
121                                 allPostReplies[postReply.id] = postReply
122                         }
123                         soneAlbums.putAll(sone.id, toAllAlbums.apply(sone)!!)
124                         for (album in toAllAlbums.apply(sone)!!) {
125                                 allAlbums[album.id] = album
126                         }
127                         soneImages.putAll(sone.id, toAllImages.apply(sone)!!)
128                         for (image in toAllImages.apply(sone)!!) {
129                                 allImages[image.id] = image
130                         }
131                 }
132         }
133
134         override fun removeSone(sone: Sone) {
135                 writeLock.withLock {
136                         allSones.remove(sone.id)
137                         val removedPosts = sonePosts.removeAll(sone.id)
138                         for (removedPost in removedPosts) {
139                                 allPosts.remove(removedPost.id)
140                         }
141                         val removedPostReplies = sonePostReplies.removeAll(sone.id)
142                         for (removedPostReply in removedPostReplies) {
143                                 allPostReplies.remove(removedPostReply.id)
144                         }
145                         val removedAlbums = soneAlbums.removeAll(sone.id)
146                         for (removedAlbum in removedAlbums) {
147                                 allAlbums.remove(removedAlbum.id)
148                         }
149                         val removedImages = soneImages.removeAll(sone.id)
150                         for (removedImage in removedImages) {
151                                 allImages.remove(removedImage.id)
152                         }
153                 }
154         }
155
156         override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
157
158         override fun getFriends(localSone: Sone): Collection<String> =
159                         if (!localSone.isLocal) {
160                                 emptySet()
161                         } else {
162                                 memoryFriendDatabase.getFriends(localSone.id)
163                         }
164
165         override fun isFriend(localSone: Sone, friendSoneId: String) =
166                         if (!localSone.isLocal) {
167                                 false
168                         } else {
169                                 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
170                         }
171
172         override fun addFriend(localSone: Sone, friendSoneId: String) {
173                 if (!localSone.isLocal) {
174                         return
175                 }
176                 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
177         }
178
179         override fun removeFriend(localSone: Sone, friendSoneId: String) {
180                 if (!localSone.isLocal) {
181                         return
182                 }
183                 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
184         }
185
186         override fun getFollowingTime(friendSoneId: String) =
187                         memoryFriendDatabase.getFollowingTime(friendSoneId)
188
189         override fun getPost(postId: String) =
190                         readLock.withLock { allPosts[postId] }
191
192         override fun getPosts(soneId: String): Collection<Post> =
193                         sonePosts[soneId].toSet()
194
195         override fun getDirectedPosts(recipientId: String) =
196                         readLock.withLock {
197                                 allPosts.values.filter {
198                                         it.recipientId.orNull() == recipientId
199                                 }
200                         }
201
202         override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
203
204         override fun storePost(post: Post) {
205                 checkNotNull(post, "post must not be null")
206                 writeLock.withLock {
207                         allPosts[post.id] = post
208                         sonePosts[post.sone.id].add(post)
209                 }
210         }
211
212         override fun removePost(post: Post) {
213                 checkNotNull(post, "post must not be null")
214                 writeLock.withLock {
215                         allPosts.remove(post.id)
216                         sonePosts[post.sone.id].remove(post)
217                         post.sone.removePost(post)
218                 }
219         }
220
221         override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
222
223         override fun getReplies(postId: String) =
224                         readLock.withLock {
225                                 allPostReplies.values
226                                                 .filter { it.postId == postId }
227                                                 .sortedWith(TIME_COMPARATOR)
228                         }
229
230         override fun newPostReplyBuilder(): PostReplyBuilder =
231                         MemoryPostReplyBuilder(this, this)
232
233         override fun storePostReply(postReply: PostReply) =
234                         writeLock.withLock {
235                                 allPostReplies[postReply.id] = postReply
236                         }
237
238         override fun removePostReply(postReply: PostReply) =
239                         writeLock.withLock {
240                                 allPostReplies.remove(postReply.id)
241                         }.unit
242
243         override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
244
245         override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
246
247         override fun storeAlbum(album: Album) =
248                         writeLock.withLock {
249                                 allAlbums[album.id] = album
250                                 soneAlbums.put(album.sone.id, album)
251                         }.unit
252
253         override fun removeAlbum(album: Album) =
254                         writeLock.withLock {
255                                 allAlbums.remove(album.id)
256                                 soneAlbums.remove(album.sone.id, album)
257                         }.unit
258
259         override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
260
261         override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
262
263         override fun storeImage(image: Image): Unit =
264                         writeLock.withLock {
265                                 allImages[image.id] = image
266                                 soneImages.put(image.sone.id, image)
267                         }
268
269         override fun removeImage(image: Image): Unit =
270                         writeLock.withLock {
271                                 allImages.remove(image.id)
272                                 soneImages.remove(image.sone.id, image)
273                         }
274
275         override fun bookmarkPost(post: Post) =
276                         memoryBookmarkDatabase.bookmarkPost(post)
277
278         override fun unbookmarkPost(post: Post) =
279                         memoryBookmarkDatabase.unbookmarkPost(post)
280
281         override fun isPostBookmarked(post: Post) =
282                         memoryBookmarkDatabase.isPostBookmarked(post)
283
284         protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
285
286         protected fun setPostKnown(post: Post, known: Boolean): Unit =
287                         writeLock.withLock {
288                                 if (known)
289                                         knownPosts.add(post.id)
290                                 else
291                                         knownPosts.remove(post.id)
292                         }
293
294         protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
295
296         protected fun setPostReplyKnown(postReply: PostReply, known: Boolean): Unit =
297                         writeLock.withLock {
298                                 if (known)
299                                         knownPostReplies.add(postReply.id)
300                                 else
301                                         knownPostReplies.remove(postReply.id)
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("KnowsPosts/$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 }