Skip to content

Commit

Permalink
Merge pull request #87 from Crusader99/dev
Browse files Browse the repository at this point in the history
Bug fixes, cursor sync
  • Loading branch information
Crusader99 authored Aug 15, 2021
2 parents 38f7916 + a39ff1a commit 408b2fa
Show file tree
Hide file tree
Showing 41 changed files with 531 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package de.hsaalen.cmt.repository

import com.mongodb.client.model.PushOptions
import de.hsaalen.cmt.events.GlobalEventDispatcher
import de.hsaalen.cmt.events.server.UserDocumentChangeEvent
import de.hsaalen.cmt.events.server.UserDocumentActionEvent
import de.hsaalen.cmt.mongo.MongoDB
import de.hsaalen.cmt.mongo.TextDocument
import de.hsaalen.cmt.network.dto.objects.ContentType
Expand All @@ -14,6 +14,7 @@ import de.hsaalen.cmt.session.senderSocketId
import de.hsaalen.cmt.sql.schema.ReferenceDao
import de.hsaalen.cmt.utils.id
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.joda.time.DateTime
import org.litote.kmongo.*

/**
Expand Down Expand Up @@ -45,19 +46,20 @@ internal object DocumentRepositoryImpl : DocumentRepository {
}

// Notify event listeners
val event = UserDocumentChangeEvent(request, currentSession.userMail, currentSession.senderSocketId)
GlobalEventDispatcher.notify(event)
UserDocumentActionEvent(request, request.uuid, currentSession.userMail, currentSession.senderSocketId).let {
GlobalEventDispatcher.notify(it)
}
}

/**
* Download the content of a specific reference by uuid.
*/
override suspend fun downloadDocument(uuid: UUID): String {
override suspend fun downloadDocument(reference: UUID): String {
// Ensure user has permissions to access this document
checkAccess(currentSession.userMail, uuid)
checkAccess(currentSession.userMail, reference)

// Read document content from MongoDB
return MongoDB.getDocumentContent(uuid.value)
return MongoDB.getDocumentContent(reference.value)
}

/**
Expand All @@ -69,6 +71,7 @@ internal object DocumentRepositoryImpl : DocumentRepository {
val ref = ReferenceDao.findById(reference.id) ?: error("Reference not found: $reference")
check(ref.owner.email == userMail) { "No permissions to access document" }
check(ref.contentType == ContentType.TEXT) { "Type " + ref.contentType.name + " is no document" }
ref.dateLastModified = DateTime.now()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package de.hsaalen.cmt.repository

import de.hsaalen.cmt.network.dto.objects.UUID
import de.hsaalen.cmt.session.currentSession
import de.hsaalen.cmt.sql.schema.ReferenceDao
import de.hsaalen.cmt.storage.StorageS3
import de.hsaalen.cmt.utils.id
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.joda.time.DateTime

/**
* Server implementation of the file repository to provide access to the AWS S3 file storage.
Expand All @@ -12,16 +17,27 @@ internal object FileRepositoryImpl : FileRepository {
* Download the reference content by a specific [UUID].
*/
override suspend fun download(uuid: UUID): ByteArray {
// TODO: Ensure user has edit permissions for that file
checkHasPermissions(currentSession.userMail, uuid)
return StorageS3.downloadFile(uuid).readBytes()
}

/**
* Upload or overwrite the reference content by a specific [UUID].
*/
override suspend fun upload(uuid: UUID, content: ByteArray) {
// TODO: Ensure user has edit permissions for that file
StorageS3.uploadFile(uuid, content.inputStream(), content.size.toLong())
override suspend fun upload(reference: UUID, content: ByteArray) {
checkHasPermissions(currentSession.userMail, reference)
StorageS3.uploadFile(reference, content.inputStream(), content.size.toLong())
}

/**
* Ensure user has edit permissions for that file to upload/download.
*/
private suspend fun checkHasPermissions(userMail: String, reference: UUID) = newSuspendedTransaction {
val ref = ReferenceDao.findById(reference.id) ?: error("No reference with uuid=$reference found!")
if (ref.owner.email != userMail) {
throw SecurityException("Can not access references from different users!")
}
ref.dateLastModified = DateTime.now()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ internal object LabelRepositoryImpl : LabelRepository {
val label = findLabel(creator, labelName) ?: error("Label not found")

// Remove label from reference
val removeQuery =
LabelRefMappingTable.deleteWhere {
(LabelRefMappingTable.label eq label.id) and (LabelRefMappingTable.reference eq ref.id)
LabelRefMappingTable.deleteWhere { removeQuery }
}

// Cleanup label when used nowhere
if (LabelRefMappingDao.find(LabelRefMappingTable.label eq label.id).count() == 0L) {
if (LabelRefMappingDao.find(LabelRefMappingTable.label eq label.id).none()) {
LabelTable.deleteWhere { LabelTable.id eq label.id }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import de.hsaalen.cmt.network.dto.rsocket.ReferenceUpdateRemoveDto
import de.hsaalen.cmt.network.dto.rsocket.ReferenceUpdateRenameDto
import de.hsaalen.cmt.network.dto.server.ServerReferenceListDto
import de.hsaalen.cmt.session.currentSession
import de.hsaalen.cmt.sql.schema.ReferenceDao
import de.hsaalen.cmt.sql.schema.ReferenceTable
import de.hsaalen.cmt.sql.schema.RevisionDao
import de.hsaalen.cmt.sql.schema.UserDao
import de.hsaalen.cmt.sql.schema.*
import de.hsaalen.cmt.storage.StorageS3
import de.hsaalen.cmt.utils.id
import de.hsaalen.cmt.utils.toUUID
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.upperCase
import org.joda.time.DateTime
Expand All @@ -44,29 +43,18 @@ internal object ReferenceRepositoryImpl : ReferenceRepository {
val ref: Reference = newSuspendedTransaction {
// Create document in SQL
val creator = UserDao.findUserByEmail(userEmail)
val now = DateTime.now()
val reference = ReferenceDao.new {
this.displayName = request.displayName
this.contentType = request.contentType
this.owner = creator
this.dateLastModified = DateTime.now()
}
val revision = RevisionDao.new {
this.item = reference
this.index = 0

this.dateCreation = now
this.dateLastAccess = now
this.creator = creator
this.accessCount = 0
}

Reference(
uuid = reference.id.toUUID(),
displayName = reference.displayName,
contentType = reference.contentType,
dateCreation = revision.dateCreation.millis,
dateLastAccess = revision.dateLastAccess.millis,
dateLastAccess = reference.dateLastModified.millis,
labels = request.labels.toMutableSet()
)
}
Expand Down Expand Up @@ -124,6 +112,16 @@ internal object ReferenceRepositoryImpl : ReferenceRepository {
}

contentType = ref.contentType

// Cleanup labels when used nowhere
for (label in ref.labels) {
if (LabelRefMappingDao.find(LabelRefMappingTable.label eq label.id).count() <= 1) {
// Delete label when this is the only reference, which will be deleted now
LabelTable.deleteWhere { LabelTable.id eq label.id }
}
}

// Remove actual reference element
ref.delete()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal object Postgresql {

// Creates the tables when not existing
// Also used to test the connection to database
SchemaUtils.create(UserTable, ReferenceTable, RevisionTable, LabelTable, LabelRefMappingTable)
SchemaUtils.create(UserTable, ReferenceTable, LabelTable, LabelRefMappingTable)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ class ReferenceDao(id: EntityID<UUID>) : UUIDEntity(id) {
* Convert [ReferenceDao] to [Reference] instance to be transmitted oer network.
*/
fun toReference(): Reference {
val now = System.currentTimeMillis()
val labels = labels.map { it.labelName }.toMutableSet()
return Reference(id.toUUID(), displayName, contentType, now, now, labels)
return Reference(id.toUUID(), displayName, contentType, dateLastModified.millis, labels)
}
}

This file was deleted.

2 changes: 1 addition & 1 deletion backend-environment/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ configurations.all {

// Configure detekt code analyze tool to generate HTML report
detekt {
ignoreFailures = true // Currently only print warning
ignoreFailures = true // Only print warning
reports {
html.enabled = true
}
Expand Down
2 changes: 1 addition & 1 deletion backend-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ tasks.test {

// Configure detekt code analyze tool to generate HTML report
detekt {
ignoreFailures = true // Currently, only print warning
ignoreFailures = true // Only print warning
reports {
html.enabled = true
}
Expand Down
21 changes: 11 additions & 10 deletions backend-server/src/main/kotlin/de/hsaalen/cmt/rsocket/Connection.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package de.hsaalen.cmt.rsocket

import de.hsaalen.cmt.events.GlobalEventDispatcher
import de.hsaalen.cmt.events.server.UserDocumentChangeEvent
import de.hsaalen.cmt.events.server.UserDocumentActionEvent
import de.hsaalen.cmt.extensions.launch
import de.hsaalen.cmt.network.dto.objects.LabelChangeMode
import de.hsaalen.cmt.network.dto.objects.LineChangeMode
import de.hsaalen.cmt.network.dto.rsocket.DocumentChangeDto
import de.hsaalen.cmt.network.dto.rsocket.LabelUpdateDto
import de.hsaalen.cmt.network.dto.rsocket.LiveDto
import de.hsaalen.cmt.network.dto.rsocket.RequestReferenceDto
import de.hsaalen.cmt.network.dto.rsocket.*
import de.hsaalen.cmt.repository.DocumentRepository
import de.hsaalen.cmt.repository.LabelRepository
import de.hsaalen.cmt.session.jwt.JwtPayload
Expand Down Expand Up @@ -102,8 +99,11 @@ class Connection(socket: RSocket, private val payload: JwtPayload, val jwtToken:
withWebSocketSession(userEmail, socketId) {
input.collect {
try {
val dto: DocumentChangeDto = it.decodeProtobufData()
docRepo.modifyDocument(dto)
when (val dto: LiveDto = it.decodeProtobufData()) {
is DocumentChangeDto -> docRepo.modifyDocument(dto)
is CursorUpdateDto -> GlobalEventDispatcher.notify(UserDocumentActionEvent(dto.copy(cursorOwner = de.hsaalen.cmt.network.dto.objects.UUID(socketId)), documentUUID, userEmail, socketId)) // Notify other clients
else -> logger.warn { "Unknown document modification: " + dto::class.simpleName }
}
} catch (ex: Exception) {
logger.error("Unable to handle document change", ex)
}
Expand All @@ -112,18 +112,19 @@ class Connection(socket: RSocket, private val payload: JwtPayload, val jwtToken:
}

// Get modifications from other clients as stream
val eventFlow = events.receiveEventsAsFlow<UserDocumentChangeEvent>()
val eventFlow = events.receiveEventsAsFlow<UserDocumentActionEvent>()
.filter { it.senderSocketId != socketId }
.filter { it.reference == documentUUID }
.map { it.modification }
.filter { it.uuid == documentUUID }

// Provide live flow synchronisation
channelFlow {
channelFlow{
documentFlow.collect { send(it) }
eventFlow.collect { send(it) }
}.onCompletion {
logger.info("Cancel document editing")
events.unregisterAll()
GlobalEventDispatcher.notify(UserDocumentActionEvent(CursorUpdateDto(de.hsaalen.cmt.network.dto.objects.UUID(socketId), null), documentUUID, userEmail, socketId))
}.map { it.buildPayload() }
}
}
Expand Down
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ tasks.dokkaHtmlMultiModule {
outputDirectory.set(buildDir.resolve("api-doc"))
includes.from("README.md") // Print on index page
}

// Execute this task to generate API documentation.
val docs by tasks.registering {
dependsOn(tasks.dokkaHtmlMultiModule)
}

tasks.withType<org.jetbrains.dokka.gradle.DokkaTask>().configureEach { dependsOn("assemble") }
tasks.withType<org.jetbrains.dokka.gradle.DokkaTaskPartial>().configureEach { dependsOn("assemble") }
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.ktor.utils.io.core.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlin.reflect.KClass

/**
Expand Down Expand Up @@ -38,10 +39,20 @@ class ListenerBundle(val caller: KClass<*>?) {
* Will automatically be closed when [ListenerBundle] is unregistered.
*/
inline fun <reified SpecificEvent : Event> receiveEventsAsFlow(): Flow<SpecificEvent> {
val ch = Channel<SpecificEvent>()
listeners += EventHandler(SpecificEvent::class) {
if (it is SpecificEvent) {
ch.send(it)
return receiveEventsAsFlow(SpecificEvent::class).filterIsInstance()
}

/**
* Build a flow of events. The flow will suspend until a new event occurred.
* Will automatically be closed when [ListenerBundle] is unregistered.
*/
fun receiveEventsAsFlow(vararg eventTypes: KClass<out Event>): Flow<Event> {
val ch = Channel<Event>()
for (eventType in eventTypes) {
listeners += EventHandler(eventType) {
if (eventType.isInstance(it)) {
ch.send(it)
}
}
}
scopeElements += object : Closeable {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.hsaalen.cmt.events.server

import de.hsaalen.cmt.events.Event
import de.hsaalen.cmt.network.dto.objects.UUID
import de.hsaalen.cmt.network.dto.rsocket.LiveDto
import kotlinx.serialization.Serializable

/**
* Event to be called when a user changes the cursor position.
*/
@Serializable
data class UserDocumentActionEvent(
val modification: LiveDto,
val reference: UUID,
val senderEmail: String,
val senderSocketId: String,
) : Event

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ data class Reference(
val uuid: UUID,
var displayName: String, // Allow renaming the reference
val contentType: ContentType,
val dateCreation: Long,
val dateLastAccess: Long,
val labels: MutableSet<String>
) : Encryptable<Reference> {
Expand Down
Loading

0 comments on commit 408b2fa

Please sign in to comment.