2 * Sone - MemoryDatabase.kt - Copyright © 2013–2019 David Roden
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.
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.
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/>.
18 package net.pterodactylus.sone.database.memory
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
51 * Memory-based [PostDatabase] implementation.
54 class MemoryDatabase @Inject constructor(private val configuration: Configuration) : AbstractService(), Database {
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)
75 override val soneLoader get() = this::getSone
77 override val sones get() = readLock.withLock { allSones.values.toSet() }
79 override val localSones get() = readLock.withLock { allSones.values.filter(Sone::isLocal) }
81 override val remoteSones get() = readLock.withLock { allSones.values.filterNot(Sone::isLocal) }
83 override val bookmarkedPosts get() = memoryBookmarkDatabase.bookmarkedPosts
86 if (saveRateLimiter.tryAcquire()) {
88 saveKnownPostReplies()
92 override fun doStart() {
93 memoryBookmarkDatabase.start()
95 loadKnownPostReplies()
99 override fun doStop() {
101 memoryBookmarkDatabase.stop()
104 } catch (de1: DatabaseException) {
109 override fun newSoneBuilder() = MemorySoneBuilder(this)
111 override fun storeSone(sone: Sone) {
115 allSones[sone.id] = sone
116 sonePosts.putAll(sone.id, sone.posts)
117 for (post in sone.posts) {
118 allPosts[post.id] = post
120 sonePostReplies.putAll(sone.id, sone.replies)
121 for (postReply in sone.replies) {
122 allPostReplies[postReply.id] = postReply
124 soneAlbums.putAll(sone.id, toAllAlbums.apply(sone)!!)
125 for (album in toAllAlbums.apply(sone)!!) {
126 allAlbums[album.id] = album
128 soneImages.putAll(sone.id, toAllImages.apply(sone)!!)
129 for (image in toAllImages.apply(sone)!!) {
130 allImages[image.id] = image
135 override fun removeSone(sone: Sone) {
137 allSones.remove(sone.id)
138 val removedPosts = sonePosts.removeAll(sone.id)
139 for (removedPost in removedPosts) {
140 allPosts.remove(removedPost.id)
142 val removedPostReplies = sonePostReplies.removeAll(sone.id)
143 for (removedPostReply in removedPostReplies) {
144 allPostReplies.remove(removedPostReply.id)
146 val removedAlbums = soneAlbums.removeAll(sone.id)
147 for (removedAlbum in removedAlbums) {
148 allAlbums.remove(removedAlbum.id)
150 val removedImages = soneImages.removeAll(sone.id)
151 for (removedImage in removedImages) {
152 allImages.remove(removedImage.id)
157 override fun getSone(soneId: String) = readLock.withLock { allSones[soneId] }
159 override fun getFriends(localSone: Sone): Collection<String> =
160 if (!localSone.isLocal) {
163 memoryFriendDatabase.getFriends(localSone.id)
166 override fun isFriend(localSone: Sone, friendSoneId: String) =
167 if (!localSone.isLocal) {
170 memoryFriendDatabase.isFriend(localSone.id, friendSoneId)
173 override fun addFriend(localSone: Sone, friendSoneId: String) {
174 if (!localSone.isLocal) {
177 memoryFriendDatabase.addFriend(localSone.id, friendSoneId)
180 override fun removeFriend(localSone: Sone, friendSoneId: String) {
181 if (!localSone.isLocal) {
184 memoryFriendDatabase.removeFriend(localSone.id, friendSoneId)
187 override fun getFollowingTime(friendSoneId: String) =
188 memoryFriendDatabase.getFollowingTime(friendSoneId)
190 override fun getPost(postId: String) =
191 readLock.withLock { allPosts[postId] }
193 override fun getPosts(soneId: String): Collection<Post> =
194 sonePosts[soneId].toSet()
196 override fun getDirectedPosts(recipientId: String) =
198 allPosts.values.filter {
199 it.recipientId.orNull() == recipientId
203 override fun newPostBuilder(): PostBuilder = MemoryPostBuilder(this, this)
205 override fun storePost(post: Post) {
206 checkNotNull(post, "post must not be null")
208 allPosts[post.id] = post
209 sonePosts[post.sone.id].add(post)
213 override fun removePost(post: Post) {
214 checkNotNull(post, "post must not be null")
216 allPosts.remove(post.id)
217 sonePosts[post.sone.id].remove(post)
218 post.sone.removePost(post)
222 override fun getPostReply(id: String) = readLock.withLock { allPostReplies[id] }
224 override fun getReplies(postId: String) =
226 allPostReplies.values
227 .filter { it.postId == postId }
228 .sortedWith(TIME_COMPARATOR)
231 override fun newPostReplyBuilder(): PostReplyBuilder =
232 MemoryPostReplyBuilder(this, this)
234 override fun storePostReply(postReply: PostReply) =
236 allPostReplies[postReply.id] = postReply
239 override fun removePostReply(postReply: PostReply) =
241 allPostReplies.remove(postReply.id)
244 override fun getAlbum(albumId: String) = readLock.withLock { allAlbums[albumId] }
246 override fun newAlbumBuilder(): AlbumBuilder = AlbumBuilderImpl()
248 override fun storeAlbum(album: Album) =
250 allAlbums[album.id] = album
251 soneAlbums.put(album.sone.id, album)
254 override fun removeAlbum(album: Album) =
256 allAlbums.remove(album.id)
257 soneAlbums.remove(album.sone.id, album)
260 override fun getImage(imageId: String) = readLock.withLock { allImages[imageId] }
262 override fun newImageBuilder(): ImageBuilder = ImageBuilderImpl()
264 override fun storeImage(image: Image): Unit =
266 allImages[image.id] = image
267 soneImages.put(image.sone.id, image)
270 override fun removeImage(image: Image): Unit =
272 allImages.remove(image.id)
273 soneImages.remove(image.sone.id, image)
276 override fun bookmarkPost(post: Post) =
277 memoryBookmarkDatabase.bookmarkPost(post)
279 override fun unbookmarkPost(post: Post) =
280 memoryBookmarkDatabase.unbookmarkPost(post)
282 override fun isPostBookmarked(post: Post) =
283 memoryBookmarkDatabase.isPostBookmarked(post)
285 protected fun isPostKnown(post: Post) = readLock.withLock { post.id in knownPosts }
287 fun setPostKnown(post: Post, known: Boolean): Unit =
290 knownPosts.add(post.id)
292 knownPosts.remove(post.id)
296 protected fun isPostReplyKnown(postReply: PostReply) = readLock.withLock { postReply.id in knownPostReplies }
298 fun setPostReplyKnown(postReply: PostReply, known: Boolean): Unit =
301 knownPostReplies.add(postReply.id)
303 knownPostReplies.remove(postReply.id)
304 saveKnownPostReplies()
307 private fun loadKnownPosts() =
308 configurationLoader.loadKnownPosts()
312 knownPosts.addAll(it)
316 private fun saveKnownPosts() =
319 knownPosts.forEachIndexed { index, knownPostId ->
320 configuration.getStringValue("KnownPosts/$index/ID").value = knownPostId
322 configuration.getStringValue("KnownPosts/${knownPosts.size}/ID").value = null
324 } catch (ce1: ConfigurationException) {
325 throw DatabaseException("Could not save database.", ce1)
328 private fun loadKnownPostReplies(): Unit =
329 configurationLoader.loadKnownPostReplies().let { knownPostReplies ->
331 this.knownPostReplies.clear()
332 this.knownPostReplies.addAll(knownPostReplies)
336 private fun saveKnownPostReplies() =
339 knownPostReplies.forEachIndexed { index, knownPostReply ->
340 configuration.getStringValue("KnownReplies/$index/ID").value = knownPostReply
342 configuration.getStringValue("KnownReplies/${knownPostReplies.size}/ID").value = null
344 } catch (ce1: ConfigurationException) {
345 throw DatabaseException("Could not save database.", ce1)