From 46b7716d48ace3928aba05156af94a7354fc0acc Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Wed, 11 Dec 2024 16:24:15 -0800 Subject: [PATCH 1/5] feat: artifact metadata writes --- .../kotlin/org/openmbee/flexo/mms/Errors.kt | 2 + .../org/openmbee/flexo/mms/Namespaces.kt | 4 +- .../org/openmbee/flexo/mms/SparqlBuilder.kt | 24 +++++++++- .../openmbee/flexo/mms/routes/Artifacts.kt | 6 +++ .../mms/routes/ldp/ArtifactMetadataWrite.kt | 47 ++++++++++++++++++- 5 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt b/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt index 64c4549..f3fc402 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Errors.kt @@ -40,6 +40,8 @@ class InvalidQueryParameter(detail: String): Http400Exception("Request contains class PreconditionsForbidden(detail: String): Http400Exception("Cannot use preconditions here: $detail") +class BlankNodesNotAllowedException(detail: String=""): Http404Exception("Blank nodes not allowed here: $detail") + open class Http403Exception(layer1: AnyLayer1Context, resource: String="(unspecified)"): HttpException("User ${layer1.userId} (${layer1.groups.joinToString(", ") { "<$it>" }}) is not authorized to perform specified action on resource: $resource", HttpStatusCode.Forbidden) diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt b/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt index 3023d58..f601aaf 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Namespaces.kt @@ -7,6 +7,8 @@ import org.apache.jena.rdf.model.ResourceFactory import org.apache.jena.shared.PrefixMapping import java.net.URLEncoder +val OPENMBEE_MMS_RDF = "https://mms.openmbee.org/rdf" + class PrefixMapBuilder(other: PrefixMapBuilder?=null, setup: (PrefixMapBuilder.() -> PrefixMapBuilder)?=null) { var map = HashMap() @@ -61,7 +63,7 @@ val SPARQL_PREFIXES = PrefixMapBuilder() { "dct" to "http://purl.org/dc/terms/", ) - with("https://mms.openmbee.org/rdf") { + with(OPENMBEE_MMS_RDF) { add( "mms" to "$this/ontology/", "mms-txn" to "$this/ontology/txn.", diff --git a/src/main/kotlin/org/openmbee/flexo/mms/SparqlBuilder.kt b/src/main/kotlin/org/openmbee/flexo/mms/SparqlBuilder.kt index fc7491d..91af3e1 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/SparqlBuilder.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/SparqlBuilder.kt @@ -10,8 +10,10 @@ import java.util.* enum class UpdateOperationBlock(val id: String) { NONE(""), - DELETE("DELETE"), INSERT("INSERT"), + DELETE("DELETE"), + INSERT_DATA("INSERT_DATA"), + DELETE_DATA("DELETE_DATA"), WHERE("WHERE"), } @@ -251,6 +253,10 @@ class InsertBuilder( } } +class InsertDataBuilder( + layer1: AnyLayer1Context, + indentLevel: Int, +): PatternBuilder(layer1, indentLevel) class ConstructBuilder( private val layer1: AnyLayer1Context, @@ -409,6 +415,22 @@ class UpdateBuilder( } } + fun insertData(setup: InsertDataBuilder.() -> Unit): UpdateBuilder { + if(operationCount++ > 0) raw(";") + + previousBlock = UpdateOperationBlock.INSERT_DATA + + return raw(""" + insert data { + ${InsertDataBuilder(layer1, 4).apply{ setup() }} + + $pendingInsertDataString + } + """).apply { + pendingInsertDataString = "" + } + } + fun where(setup: WhereBuilder.() -> Unit): UpdateBuilder { previousBlock = UpdateOperationBlock.WHERE diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt index 6e02782..165f7d1 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt @@ -7,6 +7,7 @@ import org.openmbee.flexo.mms.assertPreconditions import org.openmbee.flexo.mms.processAndSubmitUserQuery import org.openmbee.flexo.mms.routes.ldp.getArtifactsMetadata import org.openmbee.flexo.mms.routes.ldp.patchArtifactsMetadata +import org.openmbee.flexo.mms.routes.ldp.postArtifactMetadata import org.openmbee.flexo.mms.routes.store.createArtifact import org.openmbee.flexo.mms.routes.store.getArtifactsStore import org.openmbee.flexo.mms.server.linkedDataPlatformDirectContainer @@ -89,6 +90,11 @@ fun Route.metadataArtifacts() { getArtifactsMetadata(true) } + // post arbitrary metadata + post { + postArtifactMetadata() + } + // method not allowed otherwiseNotAllowed("metadata artifacts") } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt index 96c5d87..1152298 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt @@ -1,11 +1,54 @@ package org.openmbee.flexo.mms.routes.ldp -import org.openmbee.flexo.mms.Layer1Context -import org.openmbee.flexo.mms.NotImplementedException +import io.ktor.server.request.* +import org.openmbee.flexo.mms.* import org.openmbee.flexo.mms.server.GenericRequest import org.openmbee.flexo.mms.server.LdpPatchResponse +import org.openmbee.flexo.mms.server.LdpPostResponse +val FORBIDDEN_ARTIFACT_SUBJECT_PREFIXES = "^".toRegex() suspend fun Layer1Context.patchArtifactsMetadata() { throw NotImplementedException("patch metadata artifact") } + +suspend fun Layer1Context.postArtifactMetadata() { + val baseIri = prefixes["mor-artifact"]!! + val content = "${prefixes}\n${call.receiveText()}" + + // create model to filter user input and stringify to triples + val triples = KModel(prefixes) { + // load user data + parseTurtle(content, this, baseIri) + + // each statement + for (stmt in this.listStatements()) { + // forbid anything that is not an IRI + if(!stmt.subject.isURIResource) { + throw BlankNodesNotAllowedException() + } + + // get resource IRI + val uri = stmt.subject.asResource().uri + + // forbidden prefix + if(uri.startsWith("urn:mms:") || uri.startsWith(ROOT_CONTEXT) || uri.startsWith(OPENMBEE_MMS_RDF)) { + throw ForbiddenPrefixException(uri) + } + } + }.stringify() + + // build SPARQL update string with INSERT DATA on Artifacts metadata graph + val updateString = buildSparqlUpdate { + insertData { + graph("mor-graph:Artifacts") { + raw(triples) + } + } + } + + // execute the update + executeSparqlUpdate(updateString) + + throw NotImplementedException("posting metadata artifact") +} From 34a31916ca70dc32e962504f08df375c0085da49 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Mon, 6 Jan 2025 10:52:34 -0800 Subject: [PATCH 2/5] dev: scratch crud and querying --- .../org/openmbee/flexo/mms/UserQuery.kt | 227 ++++++++++++++++++ .../org/openmbee/flexo/mms/routes/Scratch.kt | 55 +++++ .../flexo/mms/routes/gsp/ModelLoad.kt | 189 ++------------- .../flexo/mms/routes/gsp/ModelRead.kt | 44 ++-- .../flexo/mms/routes/sparql/ModelCommit.kt | 66 ++--- .../flexo/mms/routes/sparql/ScratchQuery.kt | 68 ++++++ .../org/openmbee/flexo/mms/server/Routing.kt | 3 + 7 files changed, 418 insertions(+), 234 deletions(-) create mode 100644 src/main/kotlin/org/openmbee/flexo/mms/routes/Scratch.kt create mode 100644 src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt diff --git a/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt b/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt index c84de31..25ae9b0 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/UserQuery.kt @@ -1,7 +1,12 @@ package org.openmbee.flexo.mms +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.content.* import io.ktor.server.request.* import io.ktor.server.response.* +import io.ktor.utils.io.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject @@ -10,11 +15,16 @@ import org.apache.jena.graph.Triple import org.apache.jena.query.QueryFactory import org.apache.jena.sparql.core.Var import org.apache.jena.sparql.engine.binding.BindingBuilder +import org.apache.jena.sparql.modify.request.* import org.apache.jena.sparql.syntax.ElementData import org.apache.jena.sparql.syntax.ElementGroup import org.apache.jena.sparql.syntax.ElementTriplesBlock import org.apache.jena.sparql.syntax.ElementUnion +import org.apache.jena.update.UpdateFactory +import org.openmbee.flexo.mms.routes.sparql.assertOperationsAllowed +import org.openmbee.flexo.mms.server.GspRequest import org.openmbee.flexo.mms.server.SparqlQueryRequest +import org.openmbee.flexo.mms.server.SparqlUpdateRequest class QuerySyntaxException(parse: Exception): Exception(parse.stackTraceToString()) @@ -30,6 +40,9 @@ suspend fun AnyLayer1Context.processAndSubmitUserQuery(queryRequest: SparqlQuery prefixes["mor"] -> { "${prefixes["mor-graph"]}Metadata" } + "${prefixes["mor-graph"]}Scratch" -> { + refIri + } else -> { null } @@ -247,3 +260,217 @@ suspend fun AnyLayer1Context.processAndSubmitUserQuery(queryRequest: SparqlQuery throw Http400Exception("Query operation not supported") } } + + +/** + * Struct for parsed SPARQL UPDATE components + */ +data class UpdateContext( + var deleteBgpString: String = "", + var insertBgpString: String = "", + var whereString: String = "", +) + +/** + * Parses a SPARQL UPDATE request from the user + */ +fun Layer1Context.parseUserUpdateString(updateString: String?=null): UpdateContext { + // parse query + val sparqlUpdateAst = try { + UpdateFactory.create(updateString?: requestContext.update) + } catch (parse: Exception) { + throw UpdateSyntaxException(parse) + } + + var deleteBgpString = "" + var insertBgpString = "" + var whereString = "" + + val operations = sparqlUpdateAst.operations + + assertOperationsAllowed(operations) + + for (update in operations) { + when (update) { + is UpdateDataDelete -> deleteBgpString = asSparqlGroup(update.quads) + is UpdateDataInsert -> insertBgpString = asSparqlGroup(update.quads) + is UpdateDeleteWhere -> { + deleteBgpString = asSparqlGroup(update.quads) + whereString = deleteBgpString + } + + is UpdateModify -> { + if (update.hasDeleteClause()) { + deleteBgpString = asSparqlGroup(update.deleteQuads) + } + + if (update.hasInsertClause()) { + insertBgpString = asSparqlGroup(update.insertQuads) + } + + whereString = asSparqlGroup(update.wherePattern.apply { + visit(NoQuadsElementVisitor) + }) + } + + is UpdateAdd -> { + throw UpdateOperationNotAllowedException("SPARQL ADD not allowed here") + } + + else -> throw UpdateOperationNotAllowedException("SPARQL ${update.javaClass.simpleName} not allowed here") + } + } + + + return UpdateContext( + deleteBgpString = deleteBgpString, + insertBgpString = insertBgpString, + whereString = whereString, + ) +} + + +/** + * Takes a Graph Store Protocol load request and forwards it to Layer 0 + */ +suspend fun Layer1Context.loadGraph(loadGraphUri: String) { + // allow client to manually pass in URL to remote file + var loadUrl: String? = call.request.queryParameters["url"] + val storeServiceUrl: String? = call.application.storeServiceUrl + + // client did not explicitly provide a URL and the store service is configured + if (loadUrl == null && storeServiceUrl != null) { + // submit a POST request to the store service endpoint + val response: HttpResponse = defaultHttpClient.post("$storeServiceUrl/$diffId") { + // TODO: verify store service request is correct and complete + // Pass received authorization to internal service + headers { + call.request.headers[HttpHeaders.Authorization]?.let { auth: String -> + append(HttpHeaders.Authorization, auth) + } + } + // stream request body from client to store service + // TODO: Handle exceptions + setBody(object : OutgoingContent.WriteChannelContent() { + override val contentType = call.request.contentType() + override val contentLength = call.request.contentLength() ?: 0L + override suspend fun writeTo(channel: ByteWriteChannel) { + call.request.receiveChannel().copyTo(channel) + } + }) + } + + // read response body + val responseText = response.bodyAsText() + + // non-200 + if (!response.status.isSuccess()) { + throw Non200Response(responseText, response.status) + } + + // set load URL + loadUrl = responseText + } + + // a load URL has been set + if (loadUrl != null) { + // parse types the store service backend accepts + val acceptTypes = parseAcceptTypes(call.application.storeServiceAccepts) + + // confirm that store service/load supports content-type + if (!acceptTypes.contains(requestContext.requestContentType)) { + throw UnsupportedMediaType("Store/LOAD backend does not support ${requestContext.responseContentType}") + } + + // use SPARQL LOAD + val loadUpdateString = buildSparqlUpdate { + raw(""" + load ?_loadUrl into graph ?_loadGraph + """) + } + + log("Loading <$loadUrl> into <$loadGraphUri> via: `$loadUpdateString`") + + executeSparqlUpdate(loadUpdateString) { + prefixes(prefixes) + + iri( + "_loadUrl" to loadUrl, + "_loadGraph" to loadGraphUri, + ) + } + + // exit + return + } + + // GSP is configured; use it + if (call.application.quadStoreGraphStoreProtocolUrl != null) { + // parse types the gsp backend accepts + val acceptTypes = parseAcceptTypes(call.application.quadStoreGraphStoreProtocolAccepts) + + // confirm that backend supports content-type + if (!acceptTypes.contains(requestContext.requestContentType)) { + throw UnsupportedMediaType("GSP backend does not support loading ${requestContext.responseContentType}") + } + + // submit a PUT request to the quad-store's GSP endpoint + val response: HttpResponse = defaultHttpClient.put(call.application.quadStoreGraphStoreProtocolUrl!!) { + // add the graph query parameter per the GSP specification + parameter("graph", loadGraphUri) + + // stream request body from client to GSP endpoint + setBody(object : OutgoingContent.WriteChannelContent() { + // forward the header for the content type, or default to turtle + override val contentType = requestContext.requestContentType + + override suspend fun writeTo(channel: ByteWriteChannel) { + call.request.receiveChannel().copyTo(channel) + } + }) + } + + // read response body + val responseText = response.bodyAsText() + + // non-200 + if (!response.status.isSuccess()) { + throw Non200Response(responseText, response.status) + } + } + // fallback to SPARQL UPDATE string + else { + // fully load request body + val body = call.receiveText() + + // parse it into a model + val model = KModel(prefixes).apply { + parseRdfByContentType(requestContext.requestContentType!!, body, this) + + // clear the prefix map so that stringified version uses full IRIs + clearNsPrefixMap() + } + + // serialize model into turtle + val loadUpdateString = buildSparqlUpdate { + insert { + // embed the model in a triples block within the update + raw(""" + # user model + graph ?_loadGraph { + ${model.stringify()} + } + """) + } + } + + // execute + executeSparqlUpdate(loadUpdateString) { + prefixes(prefixes) + + iri( + "_loadGraph" to loadGraphUri, + ) + } + } +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratch.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratch.kt new file mode 100644 index 0000000..42201f6 --- /dev/null +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratch.kt @@ -0,0 +1,55 @@ +package org.openmbee.flexo.mms.routes + +import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.openmbee.flexo.mms.loadGraph +import org.openmbee.flexo.mms.routes.gsp.RefType +import org.openmbee.flexo.mms.routes.gsp.readModel +import org.openmbee.flexo.mms.server.graphStoreProtocol + + +const val SCRATCH_PATH = "/orgs/{orgId}/repos/{repoId}/scratch" + +/** + * Scratch CRUD routing + */ +fun Route.crudScratch() { + graphStoreProtocol("$SCRATCH_PATH/graph") { + // 5.6 HEAD: check state of scratch graph + head { + readModel(RefType.SCRATCH) + } + + // 5.2 GET: read graph + get { + readModel(RefType.SCRATCH) + } + + // 5.3 PUT: overwrite (load) + put { + // load triples directly into mor-graph:Scratch + loadGraph("${prefixes["mor-graph"]}Scratch") + + // close response + call.respondText("", status = HttpStatusCode.NoContent) + } + +// // 5.5 POST: merge +// post { +// +// } + +// // 5.7 PATCH: patch +// patch { +// +// } + +// // 5.4 DELETE: delete +// delete { +// +// } + + otherwiseNotAllowed("scratch") + } +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelLoad.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelLoad.kt index a5a2adc..d89969b 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelLoad.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelLoad.kt @@ -80,177 +80,36 @@ suspend fun GspLayer1Context.loadModel() { // prepare IRI for named graph to hold loaded model val loadGraphUri = "${prefixes["mor-graph"]}Load.$transactionId" - // now load triples into designated load graph - run { - // allow client to manually pass in URL to remote file - var loadUrl: String? = call.request.queryParameters["url"] - var storeServiceUrl: String? = call.application.storeServiceUrl - - // client did not explicitly provide a URL and the store service is configured - if (loadUrl == null && storeServiceUrl != null) { - // submit a POST request to the store service endpoint - val response: HttpResponse = defaultHttpClient.post("$storeServiceUrl/$diffId") { - // TODO: verify store service request is correct and complete - // Pass received authorization to internal service - headers { - call.request.headers[HttpHeaders.Authorization]?.let { auth: String -> - append(HttpHeaders.Authorization, auth) - } - } - // stream request body from client to store service - // TODO: Handle exceptions - setBody(object : OutgoingContent.WriteChannelContent() { - override val contentType = call.request.contentType() - override val contentLength = call.request.contentLength() ?: 0L - override suspend fun writeTo(channel: ByteWriteChannel) { - call.request.receiveChannel().copyTo(channel) - } - }) - } - - // read response body - val responseText = response.bodyAsText() - - // non-200 - if (!response.status.isSuccess()) { - throw Non200Response(responseText, response.status) - } - - // set load URL - loadUrl = responseText - } - - // a load URL has been set - if (loadUrl != null) { - // parse types the store service backend accepts - val acceptTypes = parseAcceptTypes(call.application.storeServiceAccepts) - - // confirm that store service/load supports content-type - if (!acceptTypes.contains(requestContext.requestContentType)) { - throw UnsupportedMediaType("Store/LOAD backend does not support ${requestContext.responseContentType}") - } - - // use SPARQL LOAD - val loadUpdateString = buildSparqlUpdate { - raw(""" - load ?_loadUrl into graph ?_loadGraph - """.trimIndent()) - } - - log("Loading <$loadUrl> into <$loadGraphUri> via: `$loadUpdateString`") - - executeSparqlUpdate(loadUpdateString) { - prefixes(prefixes) - - iri( - "_loadUrl" to loadUrl, - "_loadGraph" to loadGraphUri, - ) - } - - // exit load block - return@run - } - - // GSP is configured; use it - if (call.application.quadStoreGraphStoreProtocolUrl != null) { - // parse types the gsp backend accepts - val acceptTypes = parseAcceptTypes(call.application.quadStoreGraphStoreProtocolAccepts) - - // confirm that backend supports content-type - if (!acceptTypes.contains(requestContext.requestContentType)) { - throw UnsupportedMediaType("GSP backend does not support loading ${requestContext.responseContentType}") - } - - // submit a PUT request to the quad-store's GSP endpoint - val response: HttpResponse = defaultHttpClient.put(call.application.quadStoreGraphStoreProtocolUrl!!) { - // add the graph query parameter per the GSP specification - parameter("graph", loadGraphUri) - - // stream request body from client to GSP endpoint - setBody(object : OutgoingContent.WriteChannelContent() { - // forward the header for the content type, or default to turtle - override val contentType = requestContext.requestContentType - - override suspend fun writeTo(channel: ByteWriteChannel) { - call.request.receiveChannel().copyTo(channel) - } - }) - } - - // read response body - val responseText = response.bodyAsText() - - // non-200 - if (!response.status.isSuccess()) { - throw Non200Response(responseText, response.status) - } - } - // fallback to SPARQL UPDATE string - else { - // fully load request body - val body = call.receiveText() - - // parse it into a model - val model = KModel(prefixes).apply { - parseRdfByContentType(requestContext.requestContentType!!, body, this) - - // clear the prefix map so that stringified version uses full IRIs - clearNsPrefixMap() - } - - // serialize model into turtle - val loadUpdateString = buildSparqlUpdate { - insert { - // embed the model in a triples block within the update - raw(""" - # user model - graph ?_loadGraph { - ${model.stringify()} - } - """) - } - } - - // execute - executeSparqlUpdate(loadUpdateString) { - prefixes(prefixes) - - iri( - "_loadGraph" to loadGraphUri, - ) - } - } - } - + // load triples into designated load graph + loadGraph(loadGraphUri) // compute the delta run { val selectQueryString = """ - select distinct ?srcGraph ?srcCommit { - graph mor-graph:Metadata { - # select the latest commit from the current named ref - ?srcRef mms:commit ?srcCommit . - - # get the latest snapshot associated with the source commit - ?srcCommit ^mms:commit/mms:snapshot ?srcSnapshot . - { - # prefer the model snapshot - ?srcSnapshot a mms:Model ; - mms:graph ?srcGraph . - } union { - # settle for staging... - ?srcSnapshot a mms:Staging ; - mms:graph ?srcGraph . - - # ...if model is not available - filter not exists { - ?srcCommit ^mms:commit/mms:snapshot/a mms:Model . - } - } + select distinct ?srcGraph ?srcCommit { + graph mor-graph:Metadata { + # select the latest commit from the current named ref + ?srcRef mms:commit ?srcCommit . + + # get the latest snapshot associated with the source commit + ?srcCommit ^mms:commit/mms:snapshot ?srcSnapshot . + { + # prefer the model snapshot + ?srcSnapshot a mms:Model ; + mms:graph ?srcGraph . + } union { + # settle for staging... + ?srcSnapshot a mms:Staging ; + mms:graph ?srcGraph . + + # ...if model is not available + filter not exists { + ?srcCommit ^mms:commit/mms:snapshot/a mms:Model . } } - """.trimIndent() + } + } + """.trimIndent() val selectResponseText = executeSparqlSelectOrAsk(selectQueryString) { prefixes(prefixes) diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt index f2caa2b..2c30397 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt @@ -1,6 +1,5 @@ package org.openmbee.flexo.mms.routes.gsp -import com.concurrentli.ManagedMultiBlocker.block import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* @@ -11,6 +10,7 @@ import org.openmbee.flexo.mms.server.GspReadResponse enum class RefType { BRANCH, LOCK, + SCRATCH, } suspend fun GspLayer1Context.readModel(refType: RefType) { @@ -20,6 +20,7 @@ suspend fun GspLayer1Context.readModel(refType: RefType) { when(refType) { RefType.BRANCH -> branch() RefType.LOCK -> lock() + RefType.SCRATCH -> {} } } @@ -37,25 +38,34 @@ suspend fun GspLayer1Context.readModel(refType: RefType) { when(refType) { RefType.BRANCH -> auth(Permission.READ_BRANCH.scope.id, BRANCH_QUERY_CONDITIONS) RefType.LOCK -> auth(Permission.READ_LOCK.scope.id, LOCK_QUERY_CONDITIONS) + RefType.SCRATCH -> auth(Permission.READ_REPO.scope.id, REPO_QUERY_CONDITIONS) } - raw(""" - graph mor-graph:Metadata { - ${when(refType) { - RefType.BRANCH -> "morb:" - RefType.LOCK -> "morl:" - }} mms:commit/^mms:commit ?ref . - - ?ref mms:snapshot ?modelSnapshot . - - ?modelSnapshot a mms:Model ; - mms:graph ?modelGraph . - } - - graph ?modelGraph { - ?s ?p ?o . + if(refType == RefType.SCRATCH) { + graph("mor-graph:Scratch") { + raw("?s ?p ?o") } - """) + } + else { + raw(""" + graph mor-graph:Metadata { + ${when(refType) { + RefType.BRANCH -> "morb:" + RefType.LOCK -> "morl:" + else -> "urn:mms:invalid" + }} mms:commit/^mms:commit ?ref . + + ?ref mms:snapshot ?modelSnapshot . + + ?modelSnapshot a mms:Model ; + mms:graph ?modelGraph . + } + + graph ?modelGraph { + ?s ?p ?o . + } + """) + } } } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ModelCommit.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ModelCommit.kt index f60faa6..c323704 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ModelCommit.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ModelCommit.kt @@ -40,54 +40,20 @@ fun Route.commitModel() { branch() } - // parse query - val sparqlUpdateAst = try { - UpdateFactory.create(requestContext.update) - } catch(parse: Exception) { - throw UpdateSyntaxException(parse) - } - - var patchString = "" - var deleteBgpString = "" - var insertBgpString = "" - var whereString = "" + val ( + deleteBgpString, + insertBgpString, + whereString, + ) = parseUserUpdateString(requestContext.update) - val operations = sparqlUpdateAst.operations - - assertOperationsAllowed(operations) - - for(update in operations) { - when(update) { - is UpdateDataDelete -> deleteBgpString = asSparqlGroup(update.quads) - is UpdateDataInsert -> insertBgpString = asSparqlGroup(update.quads) - is UpdateDeleteWhere -> { - deleteBgpString = asSparqlGroup(update.quads) - whereString = deleteBgpString - } - is UpdateModify -> { - if(update.hasDeleteClause()) { - deleteBgpString = asSparqlGroup(update.deleteQuads) - } - - if(update.hasInsertClause()) { - insertBgpString = asSparqlGroup(update.insertQuads) - } - - whereString = asSparqlGroup(update.wherePattern.apply { - visit(NoQuadsElementVisitor) - }) - } - is UpdateAdd -> { + log("Model commit update:\n\n\tINSERT: $insertBgpString\n\n\tDELETE: $deleteBgpString\n\n\tWHERE: $whereString") - } - else -> throw UpdateOperationNotAllowedException("SPARQL ${update.javaClass.simpleName} not allowed here") - } - } + var patchString = "" - if(whereString.isBlank()) { + if (whereString.isBlank()) { val patches = mutableListOf() - if(deleteBgpString.isNotBlank()) { + if (deleteBgpString.isNotBlank()) { patches.add(""" delete data { graph ?__mms_model { @@ -97,7 +63,7 @@ fun Route.commitModel() { """.trimIndent()) } - if(insertBgpString.isNotBlank()) { + if (insertBgpString.isNotBlank()) { patches.add(""" insert data { graph ?__mms_model { @@ -108,9 +74,8 @@ fun Route.commitModel() { } patchString = patches.joinToString(" ; ") - } - else { - if(deleteBgpString.isNotBlank()) { + } else { + if (deleteBgpString.isNotBlank()) { patchString += """ delete { graph ?__mms_model { @@ -120,7 +85,7 @@ fun Route.commitModel() { """.trimIndent() } - if(insertBgpString.isNotBlank()) { + if (insertBgpString.isNotBlank()) { patchString += """ insert { graph ?__mms_model { @@ -139,9 +104,6 @@ fun Route.commitModel() { """.trimIndent() } - - log("Model commit update:\n\n\tINSERT: $insertBgpString\n\n\tDELETE: $deleteBgpString\n\n\tWHERE: $whereString") - val localConditions = DEFAULT_UPDATE_CONDITIONS.append { if(whereString.isNotEmpty()) { inspect("userWhere") { @@ -354,4 +316,4 @@ fun Route.commitModel() { // log.info(dropInterimResponseText) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt new file mode 100644 index 0000000..2d71e23 --- /dev/null +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt @@ -0,0 +1,68 @@ +package org.openmbee.flexo.mms.routes.sparql + +import io.ktor.http.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import org.openmbee.flexo.mms.REPO_QUERY_CONDITIONS +import org.openmbee.flexo.mms.parseUserUpdateString +import org.openmbee.flexo.mms.processAndSubmitUserQuery +import org.openmbee.flexo.mms.routes.SCRATCH_PATH +import org.openmbee.flexo.mms.server.sparqlQuery +import org.openmbee.flexo.mms.server.sparqlUpdate + + +/** + * User submitted SPARQL Query to a scratch space + */ +fun Route.queryScratch() { + sparqlQuery("$SCRATCH_PATH/query") { + parsePathParams { + org() + repo() + branch() + inspect() + } + + processAndSubmitUserQuery(requestContext, "${prefixes["mor-graph"]}Scratch", REPO_QUERY_CONDITIONS) + } + + sparqlUpdate("$SCRATCH_PATH/update") { + parsePathParams { + org() + repo() + branch() + } + + // parse update string + val ( + deleteBgpString, + insertBgpString, + whereString, + ) = parseUserUpdateString() + + // scope the update to the scratch named graph + val updateString = buildSparqlUpdate { + delete { + graph("mor-graph:Scratch") { + raw(deleteBgpString) + } + } + insert { + graph("mor-graph:Scratch") { + raw(insertBgpString) + } + } + where { + graph("mor-graph:Scratch") { + raw(whereString) + } + } + } + + val responseText = executeSparqlUpdate(updateString) { + prefixes(prefixes) + } + + call.respondText(responseText, status = HttpStatusCode.OK) + } +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt b/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt index c166fec..0bf119f 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt @@ -99,6 +99,9 @@ fun Application.configureRouting() { metadataArtifacts() queryArtifacts() + crudScratch() + queryScratch() + crudModel() crudCollections() From c4b91234e9208fe8e198afa1c03ff388085c1644 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Mon, 6 Jan 2025 15:32:04 -0800 Subject: [PATCH 3/5] dev: rm artifact metadata --- .../openmbee/flexo/mms/routes/Artifacts.kt | 58 ------------------- .../mms/routes/ldp/ArtifactMetadataRead.kt | 11 ---- .../mms/routes/ldp/ArtifactMetadataWrite.kt | 54 ----------------- .../org/openmbee/flexo/mms/server/Routing.kt | 1 - 4 files changed, 124 deletions(-) delete mode 100644 src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataRead.kt delete mode 100644 src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt index 165f7d1..0811065 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/Artifacts.kt @@ -2,15 +2,10 @@ package org.openmbee.flexo.mms.routes import io.ktor.server.routing.* import org.openmbee.flexo.mms.ARTIFACT_QUERY_CONDITIONS -import org.openmbee.flexo.mms.NotImplementedException import org.openmbee.flexo.mms.assertPreconditions import org.openmbee.flexo.mms.processAndSubmitUserQuery -import org.openmbee.flexo.mms.routes.ldp.getArtifactsMetadata -import org.openmbee.flexo.mms.routes.ldp.patchArtifactsMetadata -import org.openmbee.flexo.mms.routes.ldp.postArtifactMetadata import org.openmbee.flexo.mms.routes.store.createArtifact import org.openmbee.flexo.mms.routes.store.getArtifactsStore -import org.openmbee.flexo.mms.server.linkedDataPlatformDirectContainer import org.openmbee.flexo.mms.server.sparqlQuery import org.openmbee.flexo.mms.server.storageAbstractionResource @@ -72,59 +67,6 @@ fun Route.storeArtifacts() { } -/** - * Artifact metadata routing - */ -fun Route.metadataArtifacts() { - // all artifacts as LDP-DC - linkedDataPlatformDirectContainer("$ARTIFACTS_PATH/metadata") { - beforeEach = { - parsePathParams { - org() - repo() - } - } - - // read all artifacts - get { - getArtifactsMetadata(true) - } - - // post arbitrary metadata - post { - postArtifactMetadata() - } - - // method not allowed - otherwiseNotAllowed("metadata artifacts") - } - - // all artifacts as LDP-DC - linkedDataPlatformDirectContainer("$ARTIFACTS_PATH/metadata/{artifactId}") { - beforeEach = { - parsePathParams { - org() - repo() - artifact() - } - } - - // read an artifact's metadata - get { - getArtifactsMetadata(false) - } - - // patch an artifact's metadata - patch { - patchArtifactsMetadata() - } - - // method not allowed - otherwiseNotAllowed("metadata artifact") - } -} - - /** * Artifact query routing */ diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataRead.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataRead.kt deleted file mode 100644 index c3649dd..0000000 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataRead.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.openmbee.flexo.mms.routes.ldp - -import org.openmbee.flexo.mms.Layer1Context -import org.openmbee.flexo.mms.NotImplementedException -import org.openmbee.flexo.mms.server.GenericRequest -import org.openmbee.flexo.mms.server.LdpReadResponse - - -suspend fun Layer1Context.getArtifactsMetadata(allArtifacts: Boolean?=false) { - throw NotImplementedException("get metadata artifact") -} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt deleted file mode 100644 index 1152298..0000000 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ArtifactMetadataWrite.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.openmbee.flexo.mms.routes.ldp - -import io.ktor.server.request.* -import org.openmbee.flexo.mms.* -import org.openmbee.flexo.mms.server.GenericRequest -import org.openmbee.flexo.mms.server.LdpPatchResponse -import org.openmbee.flexo.mms.server.LdpPostResponse - -val FORBIDDEN_ARTIFACT_SUBJECT_PREFIXES = "^".toRegex() - -suspend fun Layer1Context.patchArtifactsMetadata() { - throw NotImplementedException("patch metadata artifact") -} - -suspend fun Layer1Context.postArtifactMetadata() { - val baseIri = prefixes["mor-artifact"]!! - val content = "${prefixes}\n${call.receiveText()}" - - // create model to filter user input and stringify to triples - val triples = KModel(prefixes) { - // load user data - parseTurtle(content, this, baseIri) - - // each statement - for (stmt in this.listStatements()) { - // forbid anything that is not an IRI - if(!stmt.subject.isURIResource) { - throw BlankNodesNotAllowedException() - } - - // get resource IRI - val uri = stmt.subject.asResource().uri - - // forbidden prefix - if(uri.startsWith("urn:mms:") || uri.startsWith(ROOT_CONTEXT) || uri.startsWith(OPENMBEE_MMS_RDF)) { - throw ForbiddenPrefixException(uri) - } - } - }.stringify() - - // build SPARQL update string with INSERT DATA on Artifacts metadata graph - val updateString = buildSparqlUpdate { - insertData { - graph("mor-graph:Artifacts") { - raw(triples) - } - } - } - - // execute the update - executeSparqlUpdate(updateString) - - throw NotImplementedException("posting metadata artifact") -} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt b/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt index 0bf119f..1de0cc1 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/server/Routing.kt @@ -96,7 +96,6 @@ fun Application.configureRouting() { crudDiffs() storeArtifacts() - metadataArtifacts() queryArtifacts() crudScratch() From 476981b8c9a8611172fe15567084471ae95c5ad9 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Mon, 6 Jan 2025 16:16:56 -0800 Subject: [PATCH 4/5] dev: scratches --- deploy/src/main.ts | 10 + .../org/openmbee/flexo/mms/AccessControl.kt | 13 +- .../org/openmbee/flexo/mms/Conditions.kt | 15 ++ .../org/openmbee/flexo/mms/Layer1Context.kt | 9 +- .../org/openmbee/flexo/mms/Namespaces.kt | 19 +- .../org/openmbee/flexo/mms/RdfModeler.kt | 4 + .../mms/routes/{Scratch.kt => Scratches.kt} | 33 +++- .../flexo/mms/routes/gsp/ModelRead.kt | 4 +- .../flexo/mms/routes/ldp/ScratchRead.kt | 132 +++++++++++++ .../flexo/mms/routes/ldp/ScratchWrite.kt | 176 ++++++++++++++++++ .../flexo/mms/routes/sparql/ScratchQuery.kt | 6 +- 11 files changed, 405 insertions(+), 16 deletions(-) rename src/main/kotlin/org/openmbee/flexo/mms/routes/{Scratch.kt => Scratches.kt} (54%) create mode 100644 src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchRead.kt create mode 100644 src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchWrite.kt diff --git a/deploy/src/main.ts b/deploy/src/main.ts index 914d24e..15cf347 100644 --- a/deploy/src/main.ts +++ b/deploy/src/main.ts @@ -254,6 +254,7 @@ ds_writer.write({ super: 'Lock', }, Artifact: {}, + Scratch: {}, Snapshot: {}, Model: { @@ -318,8 +319,11 @@ ds_writer.write({ implies: [ 'Ref', 'Artifact', + 'Scratch', ] }, + Artifact: {}, + Scratch: {}, Collection: {}, Ref: { implies: [ @@ -447,6 +451,8 @@ ds_writer.write({ 'DeleteArtifact', 'CreateDiff', 'DeleteDiff', + 'CreateScratch', + 'DeleteScratch', ], }, }, @@ -485,6 +491,10 @@ ds_writer.write({ crud: H_CRUD_DEFAULT, }, + Scratch: { + crud: H_CRUD_DEFAULT, + }, + AccessControlAny: { crud: { ...H_CRUD_DEFAULT, diff --git a/src/main/kotlin/org/openmbee/flexo/mms/AccessControl.kt b/src/main/kotlin/org/openmbee/flexo/mms/AccessControl.kt index 57495b2..99ab45f 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/AccessControl.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/AccessControl.kt @@ -17,6 +17,7 @@ enum class Scope(val type: String, val id: String, vararg val extras: String) { BRANCH("Branch", "morb"), LOCK("Lock", "morl"), ARTIFACT("Artifact", "mora"), + SCRATCH("Scratch", "mors"), DIFF("Diff", "mord"), ACCESS_CONTROL_ANY("AccessControl", "ma", "ma:Agents", "ma:Policies"), @@ -66,9 +67,14 @@ enum class Permission( DELETE_LOCK(Crud.DELETE, Scope.LOCK), CREATE_ARTIFACT(Crud.CREATE, Scope.ARTIFACT), - READ_ARTIFACT(Crud.CREATE, Scope.ARTIFACT), - UPDATE_ARTIFACT(Crud.CREATE, Scope.ARTIFACT), - DELETE_ARTIFACT(Crud.CREATE, Scope.ARTIFACT), + READ_ARTIFACT(Crud.READ, Scope.ARTIFACT), + UPDATE_ARTIFACT(Crud.UPDATE, Scope.ARTIFACT), + DELETE_ARTIFACT(Crud.DELETE, Scope.ARTIFACT), + + CREATE_SCRATCH(Crud.CREATE, Scope.SCRATCH), + READ_SCRATCH(Crud.READ, Scope.SCRATCH), + UPDATE_SCRATCH(Crud.UPDATE, Scope.SCRATCH), + DELETE_SCRATCH(Crud.DELETE, Scope.SCRATCH), CREATE_DIFF(Crud.CREATE, Scope.DIFF), READ_DIFF(Crud.READ, Scope.DIFF), @@ -95,6 +101,7 @@ enum class Role(val id: String) { ADMIN_MODEL("AdminModel"), ADMIN_LOCK("AdminLock"), ADMIN_BRANCH("AdminBranch"), + ADMIN_SCRATCH("AdminScratch"), ADMIN_DIFF("AdminDiff"), ADMIN_GROUP("AdminGroup"), ADMIN_POLICY("AdminPolicy"), diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt b/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt index 621bbcd..59f9339 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt @@ -257,6 +257,21 @@ class ConditionsBuilder(val conditions: MutableList = arrayListOf()) } } + fun scratchExists() { + require("scratchExists") { + handler = { layer1 -> "Scratch <${layer1.prefixes["mors"]}> does not exist." to + if(null != layer1.ifMatch) HttpStatusCode.PreconditionFailed else HttpStatusCode.NotFound } + + """ + # scratch must exist + graph mor-graph:Metadata { + mors: a mms:Scratch ; + ?scratchExisting_p ?scratchExisting_o . + } + """ + } + } + /** * Adds a pattern to the query conditions that is only evaluated upon inspection. */ diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Layer1Context.kt b/src/main/kotlin/org/openmbee/flexo/mms/Layer1Context.kt index aa09352..303f756 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Layer1Context.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Layer1Context.kt @@ -95,6 +95,7 @@ class Layer1Context + // set org id on context + scratchId = slug + + // create new org + createOrReplaceScratch() + } + } + + // GSP specific scratch + graphStoreProtocol("$SCRATCHES_PATH/{scratchId}/graph") { // 5.6 HEAD: check state of scratch graph head { readModel(RefType.SCRATCH) @@ -29,7 +54,7 @@ fun Route.crudScratch() { // 5.3 PUT: overwrite (load) put { // load triples directly into mor-graph:Scratch - loadGraph("${prefixes["mor-graph"]}Scratch") + loadGraph("${prefixes["mor-graph"]}Scratch.$scratchId") // close response call.respondText("", status = HttpStatusCode.NoContent) @@ -50,6 +75,6 @@ fun Route.crudScratch() { // // } - otherwiseNotAllowed("scratch") + otherwiseNotAllowed("scratches") } } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt index 2c30397..42e1b73 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt @@ -20,7 +20,7 @@ suspend fun GspLayer1Context.readModel(refType: RefType) { when(refType) { RefType.BRANCH -> branch() RefType.LOCK -> lock() - RefType.SCRATCH -> {} + RefType.SCRATCH -> scratch() } } @@ -42,7 +42,7 @@ suspend fun GspLayer1Context.readModel(refType: RefType) { } if(refType == RefType.SCRATCH) { - graph("mor-graph:Scratch") { + graph("mor-graph:Scratch.$scratchId") { raw("?s ?p ?o") } } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchRead.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchRead.kt new file mode 100644 index 0000000..0e86574 --- /dev/null +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchRead.kt @@ -0,0 +1,132 @@ +package org.openmbee.flexo.mms.routes.ldp + +import io.ktor.http.* +import io.ktor.server.response.* +import org.openmbee.flexo.mms.* +import org.openmbee.flexo.mms.routes.SPARQL_VAR_NAME_SCRATCH +import org.openmbee.flexo.mms.server.LdpDcLayer1Context +import org.openmbee.flexo.mms.server.LdpGetResponse +import org.openmbee.flexo.mms.server.LdpHeadResponse + + +private const val SPARQL_VAR_NAME_CONTEXT = "_context" + +// reusable basic graph pattern for matching scratch(es) +private val SPARQL_BGP_SCRATCH = """ + graph mor-graph:Metadata { + ?$SPARQL_VAR_NAME_SCRATCH a mms:Scratch ; + mms:id ?__mmd_id ; + ?scratch_p ?scratch_o . + } + + ${permittedActionSparqlBgp(Permission.READ_SCRATCH, Scope.REPO)} +""" + +// select ID(s) of existing scratch(es) +private val SPARQL_SELECT_SCRATCH_IDS = """ + select distinct ?__mms_id { + $SPARQL_BGP_SCRATCH + } order by asc(?__mms_id) +""" + +// construct graph of all relevant scratch metadata +private val SPARQL_CONSTRUCT_SCRATCH = """ + construct { + ?$SPARQL_VAR_NAME_SCRATCH ?scratch_p ?scratch_o ; + mms:id ?__mmd_id ; + . + + ?$SPARQL_VAR_NAME_CONTEXT a mms:Context ; + mms:permit mms-object:Permission.ReadScratch ; + mms:policy ?policy ; + . + + #?__mms_policy ?__mms_policy_p ?__mms_policy_o . + + ?scratchPolicy ?scratchPolicy_p ?scratchPolicy_o . + } where { + $SPARQL_BGP_SCRATCH + + graph m-graph:AccessControl.Policies { + #?__mms_policy ?__mms_policy_p ?__mms_policy_o . + + optional { + ?scratchPolicy a mms:Policy ; + mms:scope ?$SPARQL_VAR_NAME_SCRATCH ; + ?scratchPolicy_p ?scratchPolicy_o . + } + } + } +""" + + +/** + * Tests access to the scratch(es) + */ +suspend fun LdpDcLayer1Context.headScratches(allScratches: Boolean=false) { + val scratchIri = if(allScratches) null else prefixes["mors"]!! + + // fetch all scratches + val selectResponseText = executeSparqlSelectOrAsk(SPARQL_SELECT_SCRATCH_IDS) { + acceptReplicaLag = true + + // internal query, give it all the prefixes + prefixes(prefixes) + + // get by scratchId + scratchIri?.let { + iri( + SPARQL_VAR_NAME_SCRATCH to it, + ) + } + + // bind a context IRI + iri( + SPARQL_VAR_NAME_CONTEXT to "${MMS_URNS.SUBJECT.context}:$transactionId", + ) + } + +// TODO: where are the access-control checks? + +// // parse the result bindings +// val bindings = parseSparqlResultsJsonSelect(selectResponseText) +// +// // hash all the scratch etags +// handleEtagAndPreconditions(bindings) + + // respond + call.respond(HttpStatusCode.NoContent) +} + + +/** + * Fetches scratch(es) metadata + */ +suspend fun LdpDcLayer1Context.getScratches(allScratches: Boolean=false) { + // cache whether this request is asking for all scratches + val scratchIri = if(allScratches) null else prefixes["mors"]!! + + // fetch all scratch details + val constructResponseText = executeSparqlConstructOrDescribe(SPARQL_CONSTRUCT_SCRATCH) { + acceptReplicaLag = true + + // internal query, give it all the prefixes + prefixes(prefixes) + + // get by scratchId + scratchIri?.let { + iri( + SPARQL_VAR_NAME_SCRATCH to it, + ) + } + + // bind a context IRI + iri( + SPARQL_VAR_NAME_CONTEXT to "${MMS_URNS.SUBJECT.context}:$transactionId", + ) + } + + // respond + call.respondText(constructResponseText, contentType = RdfContentTypes.Turtle) +} + diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchWrite.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchWrite.kt new file mode 100644 index 0000000..643e1ef --- /dev/null +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/ldp/ScratchWrite.kt @@ -0,0 +1,176 @@ +package org.openmbee.flexo.mms.routes.ldp + +import io.ktor.http.* +import org.apache.jena.vocabulary.RDF +import org.openmbee.flexo.mms.* +import org.openmbee.flexo.mms.server.LdpDcLayer1Context +import org.openmbee.flexo.mms.server.LdpMutateResponse + + +// require that the given scratch does not exist before attempting to create it +private fun ConditionsBuilder.scratchNotExists() { + require("scratchNotExists") { + handler = { layer1 -> "The provided scratch <${layer1.prefixes["mors"]}> already exists." to HttpStatusCode.BadRequest } + + """ + # scratch must not yet exist + filter not exists { + graph mor-graph:Metadata { + mors: a mms:Scratch . + } + } + """ + } +} + +// selects all properties of an existing scratch +private fun PatternBuilder<*>.existingScratch() { + graph("mor-graph:Metadata") { + raw(""" + mors: ?scratchExisting_p ?scratchExisting_o . + """) + } +} + +/** + * Creates or replaces scratch(s) + * + * TResponseContext generic is bound by LdpWriteResponse, which can be a response to either a PUT or POST request + */ +suspend fun LdpDcLayer1Context.createOrReplaceScratch() { + // process RDF body from user about this new scratch + val scratchTriples = filterIncomingStatements("mors") { + // relative to this scratch node + scratchNode().apply { + // sanitize statements + sanitizeCrudObject { + setProperty(RDF.type, MMS.Scratch) + setProperty(MMS.id, scratchId!!) + } + } + } + + // resolve ambiguity + if(intentIsAmbiguous) { + // ask if scratch exists + val probeResults = executeSparqlSelectOrAsk(buildSparqlQuery { + ask { + existingScratch() + } + }) + + // parse response + val exists = parseSparqlResultsJsonAsk(probeResults) + + // scratch does not exist + if(!exists) { + replaceExisting = false + } + } + + + // build conditions + val localConditions = GLOBAL_CRUD_CONDITIONS.append { + if(isPostMethod) { + // reject preconditions + if(ifMatch != null || ifNoneMatch != null) { + throw PreconditionsForbidden("when creating scratch via POST") + } + } + // not POST + else { + // resource must exist + if(mustExist) { + scratchExists() + } + + // resource must not exist + if(mustNotExist) { + scratchNotExists() + } + // resource may exist + else { +// // enforce preconditions if present +// appendPreconditions { values -> +// """ +// graph mor-graph:Metadata { +// ${if(mustExist) "" else "optional {"} +// mors: mms:id ?__mms_id . +// ${values.reindent(8)} +// ${if(mustExist) "" else "}"} +// } +// """ +// } + } + } + + // intent is ambiguous or resource is definitely being replaced + if(replaceExisting) { + // require that the user has the ability to update scratches on a repo-level scope (necessarily implies ability to create) + permit(Permission.UPDATE_SCRATCH, Scope.REPO) + } + // resource is being created + else { + // require that the user has the ability to create scratches on a repo-level scope + permit(Permission.CREATE_SCRATCH, Scope.REPO) + } + } + + // prep SPARQL UPDATE string + val updateString = buildSparqlUpdate { + if(replaceExisting) { + delete { + existingScratch() + } + } + insert { + // create a new txn object in the transactions graph + txn { + // create a new policy that grants this user admin over the new scratch + if(!replaceExisting) autoPolicy(Scope.SCRATCH, Role.ADMIN_SCRATCH) + } + + // insert the triples about the scratch, including arbitrary metadata supplied by user + graph("mor-graph:Metadata") { + raw(scratchTriples) + } + } + where { + // assert the required conditions (e.g., access-control, existence, etc.) + raw(*localConditions.requiredPatterns()) + } + } + + // execute update + executeSparqlUpdate(updateString) + + // create construct query to confirm transaction and fetch scratch details + val constructString = buildSparqlQuery { + construct { + // all the details about this transaction + txn() + + // all the properties about this scratch + raw(""" + mors: ?mors_p ?mors_o . + """) + } + where { + // first group in a series of unions fetches intended outputs + group { + txn(null, "mors") + + graph("mor-graph:Metadata") { + raw(""" + mors: ?mors_p ?mors_o . + """) + } + } + // all subsequent unions are for inspecting what if any conditions failed + raw("""union ${localConditions.unionInspectPatterns()}""") + } + } + + // finalize transaction + finalizeMutateTransaction(constructString, localConditions, "mors", !replaceExisting) +} diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt index 2d71e23..6d66955 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt @@ -6,7 +6,7 @@ import io.ktor.server.routing.* import org.openmbee.flexo.mms.REPO_QUERY_CONDITIONS import org.openmbee.flexo.mms.parseUserUpdateString import org.openmbee.flexo.mms.processAndSubmitUserQuery -import org.openmbee.flexo.mms.routes.SCRATCH_PATH +import org.openmbee.flexo.mms.routes.SCRATCHES_PATH import org.openmbee.flexo.mms.server.sparqlQuery import org.openmbee.flexo.mms.server.sparqlUpdate @@ -15,7 +15,7 @@ import org.openmbee.flexo.mms.server.sparqlUpdate * User submitted SPARQL Query to a scratch space */ fun Route.queryScratch() { - sparqlQuery("$SCRATCH_PATH/query") { + sparqlQuery("$SCRATCHES_PATH/query") { parsePathParams { org() repo() @@ -26,7 +26,7 @@ fun Route.queryScratch() { processAndSubmitUserQuery(requestContext, "${prefixes["mor-graph"]}Scratch", REPO_QUERY_CONDITIONS) } - sparqlUpdate("$SCRATCH_PATH/update") { + sparqlUpdate("$SCRATCHES_PATH/update") { parsePathParams { org() repo() From f9afc13b2ae4bea67465f4eea3b01117a3891605 Mon Sep 17 00:00:00 2001 From: Blake Regalia Date: Mon, 6 Jan 2025 16:31:13 -0800 Subject: [PATCH 5/5] dev: scratch query/update --- .../org/openmbee/flexo/mms/Conditions.kt | 4 ++ .../openmbee/flexo/mms/routes/Scratches.kt | 58 ++++++++++++++++++- .../flexo/mms/routes/gsp/ModelRead.kt | 2 +- .../flexo/mms/routes/sparql/ScratchQuery.kt | 21 ++++--- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt b/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt index 59f9339..eac5771 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/Conditions.kt @@ -124,6 +124,10 @@ val ARTIFACT_QUERY_CONDITIONS = SNAPSHOT_QUERY_CONDITIONS.append { permit(Permission.READ_ARTIFACT, Scope.ARTIFACT) } +val SCRATCH_QUERY_CONDITIONS = REPO_CRUD_CONDITIONS.append { + permit(Permission.READ_SCRATCH, Scope.SCRATCH) +} + val DIFF_QUERY_CONDITIONS = SNAPSHOT_QUERY_CONDITIONS.append { permit(Permission.READ_DIFF, Scope.DIFF) } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratches.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratches.kt index 2cb6e7f..08ec3a8 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratches.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/Scratches.kt @@ -3,9 +3,12 @@ package org.openmbee.flexo.mms.routes import io.ktor.http.* import io.ktor.server.response.* import io.ktor.server.routing.* -import org.openmbee.flexo.mms.loadGraph +import org.openmbee.flexo.mms.* import org.openmbee.flexo.mms.routes.gsp.RefType import org.openmbee.flexo.mms.routes.gsp.readModel +import org.openmbee.flexo.mms.routes.ldp.createOrReplaceScratch +import org.openmbee.flexo.mms.routes.ldp.getScratches +import org.openmbee.flexo.mms.routes.ldp.headScratches import org.openmbee.flexo.mms.server.graphStoreProtocol import org.openmbee.flexo.mms.server.linkedDataPlatformDirectContainer @@ -37,6 +40,57 @@ fun Route.crudScratch() { // create new org createOrReplaceScratch() } + + otherwiseNotAllowed("crud scratches") + } + + // specific scratch + linkedDataPlatformDirectContainer("$SCRATCHES_PATH/{scratchId}") { + // state of all scratches + head { + headScratches() + } + + // read all scratches + get { + getScratches() + } + + // create or replace scratch + put { + createOrReplaceScratch() + } + + // modify existing org + patch { + // build conditions + val localConditions = REPO_CRUD_CONDITIONS.append { + // scratch must exist + scratchExists() + + // enforce preconditions if present + appendPreconditions { values -> + """ + graph mor-graph:Metadata { + ${values.reindent(6)} + } + """ + } + + // require that the user has the ability to update this org on an org-level scope + permit(Permission.UPDATE_SCRATCH, Scope.SCRATCH) + } + + // handle all varieties of accepted PATCH request formats + guardedPatch( + updateRequest = it, + objectKey = "mors", + graph = "mor-graph:Metadata", + preconditions = localConditions, + ) + } + + otherwiseNotAllowed("crud scratch") } // GSP specific scratch @@ -75,6 +129,6 @@ fun Route.crudScratch() { // // } - otherwiseNotAllowed("scratches") + otherwiseNotAllowed("store scratch") } } diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt index 42e1b73..1a58677 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/gsp/ModelRead.kt @@ -38,7 +38,7 @@ suspend fun GspLayer1Context.readModel(refType: RefType) { when(refType) { RefType.BRANCH -> auth(Permission.READ_BRANCH.scope.id, BRANCH_QUERY_CONDITIONS) RefType.LOCK -> auth(Permission.READ_LOCK.scope.id, LOCK_QUERY_CONDITIONS) - RefType.SCRATCH -> auth(Permission.READ_REPO.scope.id, REPO_QUERY_CONDITIONS) + RefType.SCRATCH -> auth(Permission.READ_SCRATCH.scope.id, SCRATCH_QUERY_CONDITIONS) } if(refType == RefType.SCRATCH) { diff --git a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt index 6d66955..bd8ec71 100644 --- a/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt +++ b/src/main/kotlin/org/openmbee/flexo/mms/routes/sparql/ScratchQuery.kt @@ -4,6 +4,7 @@ import io.ktor.http.* import io.ktor.server.response.* import io.ktor.server.routing.* import org.openmbee.flexo.mms.REPO_QUERY_CONDITIONS +import org.openmbee.flexo.mms.SCRATCH_QUERY_CONDITIONS import org.openmbee.flexo.mms.parseUserUpdateString import org.openmbee.flexo.mms.processAndSubmitUserQuery import org.openmbee.flexo.mms.routes.SCRATCHES_PATH @@ -15,21 +16,22 @@ import org.openmbee.flexo.mms.server.sparqlUpdate * User submitted SPARQL Query to a scratch space */ fun Route.queryScratch() { - sparqlQuery("$SCRATCHES_PATH/query") { + sparqlQuery("$SCRATCHES_PATH/{scratchId}/query") { parsePathParams { org() repo() - branch() + scratch() inspect() } - processAndSubmitUserQuery(requestContext, "${prefixes["mor-graph"]}Scratch", REPO_QUERY_CONDITIONS) + processAndSubmitUserQuery(requestContext, "${prefixes["mor-graph"]}Scratch.$scratchId", SCRATCH_QUERY_CONDITIONS) } - sparqlUpdate("$SCRATCHES_PATH/update") { + sparqlUpdate("$SCRATCHES_PATH/{scratchId}/update") { parsePathParams { org() repo() + scratch() branch() } @@ -40,29 +42,34 @@ fun Route.queryScratch() { whereString, ) = parseUserUpdateString() + // construct the scratch's named graph IRI + val scratchGraph = "mor-graph:Scratch.$scratchId" + // scope the update to the scratch named graph val updateString = buildSparqlUpdate { delete { - graph("mor-graph:Scratch") { + graph(scratchGraph) { raw(deleteBgpString) } } insert { - graph("mor-graph:Scratch") { + graph(scratchGraph) { raw(insertBgpString) } } where { - graph("mor-graph:Scratch") { + graph(scratchGraph) { raw(whereString) } } } + // execute the SPARQL UPDATE val responseText = executeSparqlUpdate(updateString) { prefixes(prefixes) } + // forward response to client call.respondText(responseText, status = HttpStatusCode.OK) } }