Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(customIRIs)!: custom IRIs must contain a UUID (DSP-1763) #1884

Merged
merged 10 commits into from Jun 29, 2021
2 changes: 1 addition & 1 deletion docs/03-apis/api-admin/groups.md
Expand Up @@ -63,7 +63,7 @@ specified by the `id` in the request body as below:

```json
{
"id": "http://rdfh.ch/groups/00FF/group-with-custom-Iri",
"id": "http://rdfh.ch/groups/00FF/a95UWs71KUklnFOe1rcw1w",
"name": "GroupWithCustomIRI",
"description": "A new group with a custom IRI",
"project": "http://rdfh.ch/projects/00FF",
Expand Down
26 changes: 13 additions & 13 deletions docs/03-apis/api-admin/lists.md
Expand Up @@ -75,7 +75,7 @@ Additionally, each list can have an optional custom IRI (of [Knora IRI](../api-v

```json
{
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a new list",
"labels": [{ "value": "Neue Liste mit IRI", "language": "de"}],
Expand All @@ -90,7 +90,7 @@ The response will contain the basic information of the list, `listinfo` and an e
"children": [],
"listinfo": {
"comments": [],
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"isRootNode": true,
"labels": [
{
Expand Down Expand Up @@ -121,7 +121,7 @@ list and the IRI of the project it belongs to.
- BODY:

```json
{ "listIri": "http://rdfh.ch/lists/0001/a-list",
{ "listIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "new name for the list",
"labels": [{ "value": "a new label for the list", "language": "en"}],
Expand All @@ -139,7 +139,7 @@ The response will contain the basic information of the list, `listinfo`, without
"language": "en"
}
],
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"isRootNode": true,
"labels": [
{
Expand Down Expand Up @@ -232,7 +232,7 @@ There is no need to specify the project IRI because it is automatically extracte

```json
{
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
Expand All @@ -243,8 +243,8 @@ There is no need to specify the project IRI because it is automatically extracte
Additionally, each child node can have an optional custom IRI (of [Knora IRI](../api-v2/knora-iris.md#iris-for-data) form) specified by the `id` in the request body as below:

```json
{ "id": "http://rdfh.ch/lists/0001/a-childNode",
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
Expand All @@ -257,8 +257,8 @@ The response will contain the basic information of the node, `nodeinfo`, as belo
{
"nodeinfo": {
"comments": [],
"hasRootNode": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/a-childNode",
"hasRootNode": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"labels": [
{
"value": "New List Node",
Expand All @@ -275,7 +275,7 @@ according to the given position, the sibling nodes will be shifted. Note that `p
number of existing children.

```json
{ "parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "Inserted new child",
"position": 0,
Expand All @@ -298,7 +298,7 @@ node and the IRI of the project it belongs to.
- BODY:

```json
{ "listIri": "http://rdfh.ch/lists/0001/a-childNode",
{ "listIri": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "new node name",
"labels": [{ "value": "new node label", "language": "en"}],
Expand All @@ -317,8 +317,8 @@ The response will contain the basic information of the node as `nodeInfo` withou
"language": "en"
}
],
"hasRootNode": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/a-childNode",
"hasRootNode": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"labels": [
{
"value": "new node label",
Expand Down
16 changes: 8 additions & 8 deletions docs/03-apis/api-admin/lists_new-list-admin-routes_v1.md
Expand Up @@ -94,7 +94,7 @@ Additionally, each list can have an optional custom IRI (of [Knora IRI](../api-v

```json
{
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a new list",
"labels": [{ "value": "Neue Liste mit IRI", "language": "de"}],
Expand All @@ -110,7 +110,7 @@ The response will contain the basic information of the list, `listinfo` and an e
"children": [],
"listinfo": {
"comments": [],
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"isRootNode": true,
"labels": [
{
Expand All @@ -131,7 +131,7 @@ Furthermore, the request body should also contain the project IRI of the list an

```json
{
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
Expand All @@ -142,8 +142,8 @@ Furthermore, the request body should also contain the project IRI of the list an
Additionally, each child node can have an optional custom IRI (of [Knora IRI](../api-v2/knora-iris.md#iris-for-data) form) specified by the `id` in the request body as below:

```json
{ "id": "http://rdfh.ch/lists/0001/a-childNode",
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
Expand All @@ -157,8 +157,8 @@ The response will contain the basic information of the node, `nodeinfo`, as belo
{
"nodeinfo": {
"comments": [],
"hasRootNode": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/a-childNode",
"hasRootNode": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"labels": [
{
"value": "New List Node",
Expand All @@ -176,7 +176,7 @@ according to the given position, the sibling nodes will be shifted. Note that `p
number of existing children.

```json
{ "parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "Inserted new child",
"position": 0,
Expand Down
6 changes: 3 additions & 3 deletions docs/03-apis/api-admin/permissions.md
Expand Up @@ -58,7 +58,7 @@ the `@id` attribute which will then be assigned to the permission; otherwise the
A custom permission IRI must be `http://rdfh.ch/permissions/PROJECT_SHORTCODE/` (where `PROJECT_SHORTCODE`
is the shortcode of the project that the permission belongs to), plus a custom ID string. For example:
```
"id": "http://rdfh.ch/permissions/0001/AP-with-customIri",
"id": "http://rdfh.ch/permissions/0001/jKIYuaEUETBcyxpenUwRzQ",
```

As a response, the created administrative permission and its IRI are returned as below:
Expand Down Expand Up @@ -108,7 +108,7 @@ a resource class of a specific project:

```json
{
"id": "http://rdfh.ch/permissions/00FF/DOAP-with-customIri",
"id": "http://rdfh.ch/permissions/00FF/fSw7w1sI5IwDjEfFi1jOeQ",
"forGroup":null,
"forProject":"http://rdfh.ch/projects/00FF",
"forProperty":null,
Expand All @@ -133,7 +133,7 @@ The response contains the newly created permission and its IRI, as:
"permissionCode": 7
}
],
"iri": "http://rdfh.ch/permissions/00FF/DOAP-with-customIri"
"iri": "http://rdfh.ch/permissions/00FF/fSw7w1sI5IwDjEfFi1jOeQ"
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/03-apis/api-admin/projects.md
Expand Up @@ -89,7 +89,7 @@ Additionally, each project can have an optional custom IRI (of [Knora IRI](../ap

```json
{
"id": "http://rdfh.ch/projects/3333",
"id": "http://rdfh.ch/projects/9TaSVMUuiRhQsuWHDPr8rw",
"shortname": "newprojectWithIri",
"shortcode": "3333",
"longname": "new project with a custom IRI",
Expand Down
2 changes: 1 addition & 1 deletion docs/03-apis/api-admin/users.md
Expand Up @@ -96,7 +96,7 @@ specified by the `id` in the request body as below:

```json
{
"id" : "http://rdfh.ch/users/donaldDuck",
"id" : "http://rdfh.ch/users/FnjFfIQFVDvI7ex8zSyUyw",
"email": "donald.duck@example.org",
"givenName": "Donald",
"familyName": "Duck",
Expand Down
4 changes: 2 additions & 2 deletions docs/03-apis/api-v2/editing-resources.md
Expand Up @@ -202,13 +202,13 @@ For example:

```jsonld
{
"@id" : "http://rdfh.ch/0001/a-custom-thing",
"@id" : "http://rdfh.ch/0001/oveR1dQltEUwNrls9Lu5Rw",
"@type" : "anything:Thing",
"knora-api:attachedToProject" : {
"@id" : "http://rdfh.ch/projects/0001"
},
"anything:hasInteger" : {
"@id" : "http://rdfh.ch/0001/a-custom-thing/values/int-value-IRI",
"@id" : "http://rdfh.ch/0001/oveR1dQltEUwNrls9Lu5Rw/values/IN4R19yYR0ygi3K2VEHpUQ",
"@type" : "knora-api:IntValue",
"knora-api:intValueAsInt" : 10,
"knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ",
Expand Down
11 changes: 7 additions & 4 deletions docs/03-apis/api-v2/editing-values.md
Expand Up @@ -84,9 +84,12 @@ Permissions for the new value can be given by adding `knora-api:hasPermissions`.
}
```

Each value can have an optional custom IRI (of [Knora IRI](knora-iris.md#iris-for-data) form) specified by the `@id` attribute, a custom creation date specified by adding
`knora-api:valueCreationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)), or a custom UUID
given by `knora-api:valueHasUUID`. Each custom UUID must be [base64url-encoded](rfc:4648#section-5), without padding.
Each value can have an optional custom IRI (of [Knora IRI](knora-iris.md#iris-for-data) form) specified by the `@id` attribute,
a custom creation date specified by adding `knora-api:valueCreationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)),
or a custom UUID given by `knora-api:valueHasUUID`. Each custom UUID must be [base64url-encoded](rfc:4648#section-5), without padding.
If a custom UUID is provided, it will be used in value IRI. If a custom IRI is given for the value, its UUID should match
the given custom UUID. If a custom IRI is provided, but there
is no custom UUID provided, then the UUID given in the IRI will be assigned to the `knora-api:valueHasUUID`.
A custom value IRI must be the IRI of the containing resource, followed
by a `/values/` and a custom ID string. For example:

Expand All @@ -95,7 +98,7 @@ by a `/values/` and a custom ID string. For example:
"@id" : "http://rdfh.ch/0001/a-thing",
"@type" : "anything:Thing",
"anything:hasInteger" : {
"@id" : "http://rdfh.ch/0001/a-thing/values/int-value-IRI",
"@id" : "http://rdfh.ch/0001/a-thing/values/IN4R19yYR0ygi3K2VEHpUQ",
"@type" : "knora-api:IntValue",
"knora-api:intValueAsInt" : 21,
"knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ",
Expand Down
2 changes: 1 addition & 1 deletion docs/03-apis/api-v2/knora-iris.md
Expand Up @@ -225,7 +225,7 @@ follows:
`http://rdfh.ch/PROJECT_SHORTCODE/mappings/MAPPING_NAME`
- XML-to-standoff mapping element:
`http://rdfh.ch/PROJECT_SHORTCODE/mappings/MAPPING_NAME/elements/MAPPING_ELEMENT_UUID`
- Project: `http://rdfh.ch/projects/PROJECT_SHORTCODE`
- Project: `http://rdfh.ch/projects/PROJECT_SHORTCODE` (or `http://rdfh.ch/projects/PROJECT_UUID`)
- Group: `http://rdfh.ch/groups/PROJECT_SHORTCODE/GROUP_UUID`
- Permission:
`http://rdfh.ch/permissions/PROJECT_SHORTCODE/PERMISSION_UUID`
Expand Down
Expand Up @@ -3068,11 +3068,15 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
* Creates a new value IRI based on a UUID.
*
* @param resourceIri the IRI of the resource that will contain the value.
* @param givenUUID the optional given UUID of the value. If not provided, create a random one.
* @return a new value IRI.
*/
def makeRandomValueIri(resourceIri: IRI): IRI = {
val knoraValueUuid = makeRandomBase64EncodedUuid
s"$resourceIri/values/$knoraValueUuid"
def makeRandomValueIri(resourceIri: IRI, givenUUID: Option[UUID] = None): IRI = {
subotic marked this conversation as resolved.
Show resolved Hide resolved
val valueUUID = givenUUID match {
case Some(uuid: UUID) => base64EncodeUuid(uuid)
case _ => makeRandomBase64EncodedUuid
}
s"$resourceIri/values/$valueUUID"
}

/**
Expand Down
Expand Up @@ -20,13 +20,12 @@
package org.knora.webapi
package responders

import exceptions.{DuplicateValueException, UnexpectedMessageException}
import exceptions.{BadRequestException, DuplicateValueException, UnexpectedMessageException}
import messages.store.triplestoremessages.SparqlSelectRequest
import messages.util.ResponderData
import messages.util.rdf.SparqlSelectResult
import messages.{SmartIri, StringFormatter}
import settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl}

import akka.actor.{ActorRef, ActorSystem}
import akka.event.LoggingAdapter
import akka.http.scaladsl.util.FastFuture
Expand Down Expand Up @@ -174,14 +173,23 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging {
* @return IRI of the entity.
*/
protected def checkOrCreateEntityIri(entityIri: Option[SmartIri], iriFormatter: => IRI): Future[IRI] = {

entityIri match {
case Some(customResourceIri) =>
case Some(customEntityIri: SmartIri) =>
val entityIriAsString = customEntityIri.toString
for {
result <- stringFormatter.checkIriExists(customResourceIri.toString, storeManager)

result <- stringFormatter.checkIriExists(entityIriAsString, storeManager)
_ = if (result) {
throw DuplicateValueException(s"IRI: '${customResourceIri.toString}' already exists, try another one.")
throw DuplicateValueException(s"IRI: '$entityIriAsString' already exists, try another one.")
}
} yield customResourceIri.toString
// Check that given entityIRI ends with a UUID
subotic marked this conversation as resolved.
Show resolved Hide resolved
ending: String = entityIriAsString.split('/').last
_ = stringFormatter.validateBase64EncodedUuid(
ending,
throw BadRequestException(s"IRI: '$entityIriAsString' must end with a valid base 64 UUID."))

} yield entityIriAsString

case None => stringFormatter.makeUnusedIri(iriFormatter, storeManager, loggingAdapter)
}
Expand Down
Expand Up @@ -31,7 +31,6 @@ import org.knora.webapi.exceptions._
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.instrumentation.InstrumentationSupport
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.StringFormatter.IriDomain
import org.knora.webapi.messages.admin.responder.projectsmessages._
import org.knora.webapi.messages.admin.responder.usersmessages.{
UserADM,
Expand Down Expand Up @@ -982,11 +981,9 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
*/
def createPermissionsForAdminsAndMembersOfNewProject(projectIri: IRI, projectShortCode: String): Future[Unit] =
for {
baseIri: String <- Future.successful(s"http://$IriDomain/permissions/$projectShortCode/")
// Give the admins of the new project rights for any operation in project level, and rights to create resources.
apPermissionForProjectAdmin: AdministrativePermissionCreateResponseADM <- (responderManager ? AdministrativePermissionCreateRequestADM(
_ <- (responderManager ? AdministrativePermissionCreateRequestADM(
createRequest = CreateAdministrativePermissionAPIRequestADM(
id = Some(baseIri + "defaultApForAdmin"),
forProject = projectIri,
forGroup = OntologyConstants.KnoraAdmin.ProjectAdmin,
hasPermissions =
Expand All @@ -998,9 +995,8 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
)).mapTo[AdministrativePermissionCreateResponseADM]

// Give the members of the new project rights to create resources.
apPermissionForProjectMember: AdministrativePermissionCreateResponseADM <- (responderManager ? AdministrativePermissionCreateRequestADM(
_ <- (responderManager ? AdministrativePermissionCreateRequestADM(
createRequest = CreateAdministrativePermissionAPIRequestADM(
id = Some(baseIri + "defaultApForMember"),
forProject = projectIri,
forGroup = OntologyConstants.KnoraAdmin.ProjectMember,
hasPermissions = Set(PermissionADM.ProjectResourceCreateAllPermission)
Expand All @@ -1012,9 +1008,8 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo

// Give the admins of the new project rights to change rights, modify, delete, view,
// and restricted view of all resources and values that belong to the project.
doapForProjectAdmin <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
_ <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
createRequest = CreateDefaultObjectAccessPermissionAPIRequestADM(
id = Some(baseIri + "defaultDoapForAdmin"),
forProject = projectIri,
forGroup = Some(OntologyConstants.KnoraAdmin.ProjectAdmin),
hasPermissions = Set(
Expand All @@ -1032,9 +1027,8 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo

// Give the members of the new project rights to modify, view, and restricted view of all resources and values
// that belong to the project.
doapForProjectMember <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
_ <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
createRequest = CreateDefaultObjectAccessPermissionAPIRequestADM(
id = Some(baseIri + "defaultDoapForMember"),
forProject = projectIri,
forGroup = Some(OntologyConstants.KnoraAdmin.ProjectMember),
hasPermissions = Set(
Expand Down Expand Up @@ -1109,7 +1103,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
)
.toString

createProjectResponse <- (storeManager ? SparqlUpdateRequest(createNewProjectSparqlString))
_ <- (storeManager ? SparqlUpdateRequest(createNewProjectSparqlString))
.mapTo[SparqlUpdateResponse]

// try to retrieve newly created project (will also add to cache)
Expand Down