diff --git a/build.sbt b/build.sbt index 68caf0ff97..22b633d546 100644 --- a/build.sbt +++ b/build.sbt @@ -103,10 +103,6 @@ lazy val sipi: Project = Project(id = "sipi", base = file("sipi")) Universal / mappings ++= { directory("sipi/scripts") }, - dockerCommands += Cmd( - "RUN", - "mv /sipi/scripts/entrypoint.sh /sipi/ && chmod +x /sipi/entrypoint.sh && apt-get update && apt-get install -y cron curl && rm -rf /var/lib/apt/lists/*" - ), // install cron and curl for periodically cleaning temp dir dockerCommands += Cmd( """HEALTHCHECK --interval=30s --timeout=30s --retries=4 --start-period=30s \ |CMD bash /sipi/scripts/healthcheck.sh || exit 1""".stripMargin @@ -125,10 +121,7 @@ lazy val sipi: Project = Project(id = "sipi", base = file("sipi")) // don't filter the rest; don't filter out anything that doesn't match a pattern case cmd => false - }, - // add our own entrypoint and also cmd because it is reset when overriding the entrypoint - dockerCommands += ExecCmd("ENTRYPOINT", "/sipi/entrypoint.sh"), - dockerCommands += ExecCmd("CMD", "--config=/sipi/config/sipi.config.lua") + } ) ////////////////////////////////////// diff --git a/docs/05-internals/design/api-v2/gravsearch.md b/docs/05-internals/design/api-v2/gravsearch.md index b19709cca1..cd9db2b82a 100644 --- a/docs/05-internals/design/api-v2/gravsearch.md +++ b/docs/05-internals/design/api-v2/gravsearch.md @@ -8,10 +8,6 @@ For a detailed overview of Gravsearch, see [Gravsearch: Transforming SPARQL to query humanities data](https://doi.org/10.3233/SW-200386). -## Gravsearch Package - -The classes that process Gravsearch queries and results can be found in `org.knora.webapi.messages.util.search.gravsearch`. - ## Type Inspection The code that converts Gravsearch queries into SPARQL queries, and processes the query results, needs to know the @@ -123,12 +119,9 @@ To improve query performance, this trait defines the method `optimiseQueryPatter private methods to optimise the generated SPARQL. For example, before transformation of statements in WHERE clause, query pattern orders must be optimised by moving `LuceneQueryPatterns` to the beginning and `isDeleted` statement patterns to the end of the WHERE clause. -- `ConstructToSelectTransformer` (extends `WhereTransformer`): instructions how to turn a Construct query into a Select query (converts a Gravsearch query into a prequery) -- `SelectToSelectTransformer` (extends `WhereTransformer`): instructions how to turn a triplestore independent Select query into a triplestore dependent Select query (implementation of inference). -- `ConstructToConstructTransformer` (extends `WhereTransformer`): instructions how to turn a triplestore independent Construct query into a triplestore dependent Construct query (implementation of inference). - -The traits listed above define methods that are implemented in the transformer classes and called by `QueryTraverser` to perform SPARQL to SPARQL conversions. -When iterating over the statements of the input query, the transformer class' transformation methods are called to perform the conversion. +- `AbstractPrequeryGenerator` (extends `WhereTransformer`): converts a Gravsearch query into a prequery; this one has two implementations for regular search queries and for count queries. +- `SelectTransformer` (extends `WhereTransformer`): transforms a Select query into a Select query with simulated RDF inference. +- `ConstructTransformer`: transforms a Construct query into a Construct query with simulated RDF inference. ### Prequery @@ -243,9 +236,6 @@ the main query can specifically ask for more detailed information on these resou #### Generating the Main Query -The classes involved in generating the main query can be found in -`org.knora.webapi.messages.util.search.gravsearch.mainquery`. - The main query is a SPARQL CONSTRUCT query. Its generation is handled by the method `GravsearchMainQueryGenerator.createMainQuery`. It takes three arguments: @@ -291,8 +281,8 @@ to the maximum allowed page size, the predicate Gravsearch queries support a subset of RDFS reasoning (see [Inference](../../../03-endpoints/api-v2/query-language.md#inference) in the API documentation on Gravsearch). This is implemented as follows: -To simulate RDF inference, the API expands the prequery on basis of the available ontologies. For that reason, `SparqlTransformer.transformStatementInWhereForNoInference` expands all `rdfs:subClassOf` and `rdfs:subPropertyOf` statements using `UNION` statements for all subclasses and subproperties from the ontologies (equivalent to `rdfs:subClassOf*` and `rdfs:subPropertyOf*`). -Similarly, `SparqlTransformer.transformStatementInWhereForNoInference` replaces `knora-api:standoffTagHasStartAncestor` with `knora-base:standoffTagHasStartParent*`. +To simulate RDF inference, the API expands all `rdfs:subClassOf` and `rdfs:subPropertyOf` statements using `UNION` statements for all subclasses and subproperties from the ontologies (equivalent to `rdfs:subClassOf*` and `rdfs:subPropertyOf*`). +Similarly, the API replaces `knora-api:standoffTagHasStartAncestor` with `knora-base:standoffTagHasStartParent*`. # Optimisation of generated SPARQL @@ -453,7 +443,7 @@ CONSTRUCT { ?thing anything:hasOtherThing ?thing1 . ?thing1 anything:hasOtherThing ?thing2 . ?thing2 anything:hasOtherThing ?thing . - +} ``` In this case, the algorithm tries to break the cycles in order to sort the graph. If this is not possible, diff --git a/integration/src/test/scala/org/knora/webapi/ITTestDataFactory.scala b/integration/src/test/scala/org/knora/webapi/ITTestDataFactory.scala index f42ead3c00..015318d730 100644 --- a/integration/src/test/scala/org/knora/webapi/ITTestDataFactory.scala +++ b/integration/src/test/scala/org/knora/webapi/ITTestDataFactory.scala @@ -5,7 +5,6 @@ package org.knora.webapi -import dsp.valueobjects.Iri._ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM._ /** @@ -26,9 +25,4 @@ object ITTestDataFactory { IriIdentifier .fromString(iri) .getOrElse(throw new IllegalArgumentException(s"Invalid IriIdentifier $iri.")) - - def projectIri(iri: String): ProjectIri = - ProjectIri - .make(iri) - .getOrElse(throw new IllegalArgumentException(s"Invalid ProjectIri $iri.")) } diff --git a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala index 1e03bc545c..c98e1c3833 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -52,9 +52,9 @@ import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.IIIFRequestMessageHandlerLive -import org.knora.webapi.store.iiif.api.IIIFService -import org.knora.webapi.store.iiif.impl.IIIFServiceMockImpl -import org.knora.webapi.store.iiif.impl.IIIFServiceSipiImpl +import org.knora.webapi.store.iiif.api.SipiService +import org.knora.webapi.store.iiif.impl.SipiServiceLive +import org.knora.webapi.store.iiif.impl.SipiServiceMock import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.impl.TriplestoreServiceLive import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater @@ -70,7 +70,7 @@ object LayersTest { type DefaultTestEnvironmentWithoutSipi = LayersLive.DspEnvironmentLive with FusekiTestContainer with TestClientService type DefaultTestEnvironmentWithSipi = DefaultTestEnvironmentWithoutSipi with SipiTestContainer - type CommonR0 = ActorSystem with AppConfigurations with IIIFService with JwtService with StringFormatter + type CommonR0 = ActorSystem with AppConfigurations with SipiService with JwtService with StringFormatter type CommonR = ApiRoutes with AppRouter @@ -198,29 +198,30 @@ object LayersTest { with SipiTestContainer with AppConfigurations with JwtService - with IIIFService + with SipiService with StringFormatter ]( AppConfigForTestContainers.testcontainers, FusekiTestContainer.layer, SipiTestContainer.layer, - IIIFServiceSipiImpl.layer, + SipiServiceLive.layer, JwtServiceLive.layer, StringFormatter.test ) private val fusekiTestcontainers = - ZLayer.make[FusekiTestContainer with AppConfigurations with JwtService with IIIFService with StringFormatter]( + ZLayer.make[FusekiTestContainer with AppConfigurations with JwtService with SipiService with StringFormatter]( AppConfigForTestContainers.fusekiOnlyTestcontainer, FusekiTestContainer.layer, - IIIFServiceMockImpl.layer, + SipiServiceMock.layer, JwtServiceLive.layer, StringFormatter.test ) /** * Provides a layer for integration tests which depend on Fuseki as Testcontainers. - * Sipi/IIIFService will be mocked with the [[IIIFServiceMockImpl]] + * Sipi/IIIFService will be mocked with the [[SipiServiceMock]] + * * @param system An optional [[pekko.actor.ActorSystem]] for use with Akka's [[pekko.testkit.TestKit]] * @return a [[ULayer]] with the [[DefaultTestEnvironmentWithoutSipi]] */ diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala index a438ea4879..08a0f42923 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala @@ -5,7 +5,9 @@ package org.knora.webapi.e2e.v2 -import org.apache.pekko +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.http.scaladsl.model.headers.BasicHttpCredentials +import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.diff.Diff @@ -39,10 +41,6 @@ import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.util._ -import pekko.http.scaladsl.model._ -import pekko.http.scaladsl.model.headers.BasicHttpCredentials -import pekko.http.scaladsl.unmarshalling.Unmarshal - class ValuesRouteV2E2ESpec extends E2ESpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -810,7 +808,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { valueType should ===(KnoraApiV2Complex.IntValue.toSmartIri) integerValueUUID = responseJsonDoc.body.requireStringWithValidation( KnoraApiV2Complex.ValueHasUUID, - UuidUtil.validateBase64EncodedUuid + (key, errorFun) => UuidUtil.base64Decode(key).getOrElse(errorFun) ) val savedValue: JsonLDObject = getValue( @@ -3102,7 +3100,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { valueType should ===(KnoraApiV2Complex.LinkValue.toSmartIri) linkValueUUID = responseJsonDoc.body.requireStringWithValidation( KnoraApiV2Complex.ValueHasUUID, - UuidUtil.validateBase64EncodedUuid + (key, errorFun) => UuidUtil.base64Decode(key).getOrElse(errorFun) ) val savedValue: JsonLDObject = getValue( @@ -3236,7 +3234,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val newIntegerValueUUID: UUID = responseJsonDoc.body.requireStringWithValidation( KnoraApiV2Complex.ValueHasUUID, - UuidUtil.validateBase64EncodedUuid + (key, errorFun) => UuidUtil.base64Decode(key).getOrElse(errorFun) ) assert(newIntegerValueUUID == integerValueUUID) // The new version should have the same UUID. @@ -4942,7 +4940,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val newLinkValueUUID: UUID = responseJsonDoc.body.requireStringWithValidation( KnoraApiV2Complex.ValueHasUUID, - UuidUtil.validateBase64EncodedUuid + (key, errorFun) => UuidUtil.base64Decode(key).getOrElse(errorFun) ) assert(newLinkValueUUID != linkValueUUID) linkValueUUID = newLinkValueUUID @@ -5016,7 +5014,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val newLinkValueUUID: UUID = responseJsonDoc.body.requireStringWithValidation( KnoraApiV2Complex.ValueHasUUID, - UuidUtil.validateBase64EncodedUuid + (key, errorFun) => UuidUtil.base64Decode(key).getOrElse(errorFun) ) assert(newLinkValueUUID == linkValueUUID) @@ -5069,7 +5067,7 @@ class ValuesRouteV2E2ESpec extends E2ESpec { val newLinkValueUUID: UUID = responseJsonDoc.body.requireStringWithValidation( KnoraApiV2Complex.ValueHasUUID, - UuidUtil.validateBase64EncodedUuid + (key, errorFun) => UuidUtil.base64Decode(key).getOrElse(errorFun) ) assert(newLinkValueUUID == linkValueUUID) diff --git a/integration/src/test/scala/org/knora/webapi/messages/StringFormatterSpec.scala b/integration/src/test/scala/org/knora/webapi/messages/StringFormatterSpec.scala index 0f1962cd4d..8d71880387 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/StringFormatterSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/StringFormatterSpec.scala @@ -948,24 +948,7 @@ class StringFormatterSpec extends CoreSpec { } - "The StringFormatter class for User and Project" should { - "validate project shortcode" in { - stringFormatter.validateProjectShortcode("00FF", throw AssertionException("not valid")) should be("00FF") - stringFormatter.validateProjectShortcode("00ff", throw AssertionException("not valid")) should be("00FF") - stringFormatter.validateProjectShortcode("12aF", throw AssertionException("not valid")) should be("12AF") - - an[AssertionException] should be thrownBy { - stringFormatter.validateProjectShortcode("000", throw AssertionException("not valid")) - } - - an[AssertionException] should be thrownBy { - stringFormatter.validateProjectShortcode("00000", throw AssertionException("not valid")) - } - - an[AssertionException] should be thrownBy { - stringFormatter.validateProjectShortcode("wxyz", throw AssertionException("not valid")) - } - } + "The StringFormatter class for User" should { "validate username" in { // 4 - 50 characters long diff --git a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala index 9161333946..6c3877aff2 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsMessagesADMSpec.scala @@ -20,6 +20,7 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralSequence import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.sharedtestdata.SharedListsTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri /** * This spec is used to test 'ListAdminMessages'. @@ -126,7 +127,7 @@ class ListsMessagesADMSpec extends CoreSpec with ListADMJsonProtocol { ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( parentNodeIri = ListIri.make(exampleListIri).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(SharedTestDataADM.imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(SharedTestDataADM.imagesProjectIri), position = Some(Position.make(-3).fold(e => throw e.head, v => v)), labels = Labels .make(Seq(V2.StringLiteralV2(value = "New child node", language = Some("en")))) diff --git a/integration/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformerSpec.scala b/integration/src/test/scala/org/knora/webapi/messages/util/search/SparqlTransformerSpec.scala similarity index 99% rename from integration/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformerSpec.scala rename to integration/src/test/scala/org/knora/webapi/messages/util/search/SparqlTransformerSpec.scala index bb5f0395a1..6a62b134f5 100644 --- a/integration/src/test/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformerSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/messages/util/search/SparqlTransformerSpec.scala @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.knora.webapi.messages.util.search.gravsearch.transformers +package org.knora.webapi.util.search import org.knora.webapi.CoreSpec import org.knora.webapi.messages.IriConversions._ @@ -80,11 +80,11 @@ class SparqlTransformerSpec extends CoreSpec { isDeletedStatement, linkStatement ) - val optimisedPatterns = SparqlTransformer.optimiseIsDeletedWithMinus(patterns) + val optimisedPatterns = SparqlTransformer.optimiseIsDeletedWithFilter(patterns) val expectedPatterns = Seq( typeStatement, linkStatement, - MinusPattern( + FilterNotExistsPattern( Seq( StatementPattern( subj = QueryVariable("foo"), diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala index eeb494582b..08c79e9439 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/GroupsResponderADMSpec.scala @@ -20,6 +20,7 @@ import org.knora.webapi.messages.admin.responder.groupsmessages._ import org.knora.webapi.messages.admin.responder.usersmessages.UserInformationTypeADM import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.util.MutableTestIri import pekko.actor.Status.Failure @@ -80,7 +81,7 @@ class GroupsResponderADMSpec extends CoreSpec { ) ) .fold(e => throw e.head, v => v), - project = ProjectIri.make(SharedTestDataADM.imagesProjectIri).fold(e => throw e.head, v => v), + project = ProjectIri.unsafeFrom(SharedTestDataADM.imagesProjectIri), status = GroupStatus.active, selfjoin = GroupSelfJoin.make(false).fold(e => throw e.head, v => v) ), @@ -113,7 +114,7 @@ class GroupsResponderADMSpec extends CoreSpec { descriptions = GroupDescriptions .make(Seq(V2.StringLiteralV2(value = "NewGroupDescription", language = Some("en")))) .fold(e => throw e.head, v => v), - project = ProjectIri.make(SharedTestDataADM.imagesProjectIri).fold(e => throw e.head, v => v), + project = ProjectIri.unsafeFrom(SharedTestDataADM.imagesProjectIri), status = GroupStatus.active, selfjoin = GroupSelfJoin.make(false).fold(e => throw e.head, v => v) ), diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala index c0f67bf89e..a252af4dc1 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ListsResponderADMSpec.scala @@ -5,7 +5,8 @@ package org.knora.webapi.responders.admin -import org.apache.pekko +import org.apache.pekko.actor.Status.Failure +import org.apache.pekko.testkit._ import java.util.UUID @@ -25,11 +26,9 @@ import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.sharedtestdata.SharedListsTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM2._ +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.util.MutableTestIri -import pekko.actor.Status.Failure -import pekko.testkit._ - /** * Tests [[ListsResponderADM]]. */ @@ -155,7 +154,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { "create a list" in { appActor ! ListRootNodeCreateRequestADM( createRootNode = ListRootNodeCreatePayloadADM( - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make("neuelistename").fold(e => throw e.head, v => v)), labels = Labels .make(Seq(V2.StringLiteralV2(value = "Neue Liste", language = Some("de")))) @@ -195,7 +194,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { val nameWithSpecialCharacter = "a new \\\"name\\\"" appActor ! ListRootNodeCreateRequestADM( createRootNode = ListRootNodeCreatePayloadADM( - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make(nameWithSpecialCharacter).fold(e => throw e.head, v => v)), labels = Labels .make(Seq(V2.StringLiteralV2(value = labelWithSpecialCharacter, language = Some("de")))) @@ -235,7 +234,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { listIri = newListIri.get, changeNodeRequest = ListNodeChangePayloadADM( listIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make("updated name").fold(e => throw e.head, v => v)), labels = Some( Labels @@ -289,7 +288,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { "not update basic list information if name is duplicate" in { val name = Some(ListName.make("sommer").fold(e => throw e.head, v => v)) - val projectIRI = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v) + val projectIRI = ProjectIri.unsafeFrom(imagesProjectIri) appActor ! NodeInfoChangeRequestADM( listIri = newListIri.get, changeNodeRequest = ListNodeChangePayloadADM( @@ -313,7 +312,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( parentNodeIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make("first").fold(e => throw e.head, v => v)), labels = Labels .make(Seq(V2.StringLiteralV2(value = "New First Child List Node Value", language = Some("en")))) @@ -366,7 +365,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( parentNodeIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make("second").fold(e => throw e.head, v => v)), position = Some(Position.make(0).fold(e => throw e.head, v => v)), labels = Labels @@ -420,7 +419,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( parentNodeIri = ListIri.make(secondChildIri.get).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make("third").fold(e => throw e.head, v => v)), labels = Labels .make(Seq(V2.StringLiteralV2(value = "New Third Child List Node Value", language = Some("en")))) @@ -474,7 +473,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { appActor ! ListChildNodeCreateRequestADM( createChildNodeRequest = ListChildNodeCreatePayloadADM( parentNodeIri = ListIri.make(newListIri.get).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(imagesProjectIri).fold(e => throw e.head, v => v), + projectIri = ProjectIri.unsafeFrom(imagesProjectIri), name = Some(ListName.make("fourth").fold(e => throw e.head, v => v)), position = givenPosition, labels = Labels @@ -832,7 +831,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { requestingUser = SharedTestDataADM.anythingAdminUser, apiRequestID = UUID.randomUUID ) - expectMsg(Failure(BadRequestException(s"Node ${nodeInUseIri} cannot be deleted, because it is in use."))) + expectMsg(Failure(BadRequestException(s"Node $nodeInUseIri cannot be deleted, because it is in use."))) } @@ -845,7 +844,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { ) val usedChild = "http://rdfh.ch/lists/0001/treeList10" expectMsg( - Failure(BadRequestException(s"Node ${nodeIri} cannot be deleted, because its child ${usedChild} is in use.")) + Failure(BadRequestException(s"Node $nodeIri cannot be deleted, because its child $usedChild is in use.")) ) } @@ -858,7 +857,7 @@ class ListsResponderADMSpec extends CoreSpec with ImplicitSender { apiRequestID = UUID.randomUUID ) expectMsg( - Failure(BadRequestException(s"Node ${nodeInUseInOntologyIri} cannot be deleted, because it is in use.")) + Failure(BadRequestException(s"Node $nodeInUseInOntologyIri cannot be deleted, because it is in use.")) ) } diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala index 6f2fce2107..c4e89469c1 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala @@ -44,6 +44,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "used to query for project information" should { "return information for every project excluding system projects" in { appActor ! ProjectsGetRequestADM() + val received = expectMsgType[ProjectsGetResponseADM](timeout) assert(received.projects.contains(SharedTestDataADM.imagesProject)) assert(received.projects.contains(SharedTestDataADM.incunabulaProject)) @@ -178,8 +179,8 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { SharedTestDataADM.rootUser, UUID.randomUUID() ) - val received: ProjectOperationResponseADM = expectMsgType[ProjectOperationResponseADM](timeout) + val received: ProjectOperationResponseADM = expectMsgType[ProjectOperationResponseADM](timeout) received.project.shortname should be("newproject") received.project.shortcode should be(shortcode.toUpperCase) // upper case received.project.longname should contain("project longname") @@ -195,6 +196,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { requestingUser = rootUser, apiRequestID = UUID.randomUUID() ) + // Check Administrative Permission of ProjectAdmin val receivedApAdmin: AdministrativePermissionsForProjectGetResponseADM = expectMsgType[AdministrativePermissionsForProjectGetResponseADM] @@ -304,8 +306,8 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { SharedTestDataADM.rootUser, UUID.randomUUID() ) - val received: ProjectOperationResponseADM = expectMsgType[ProjectOperationResponseADM](timeout) + val received: ProjectOperationResponseADM = expectMsgType[ProjectOperationResponseADM](timeout) received.project.longname should contain(Iri.fromSparqlEncodedString(longnameWithSpecialCharacter)) received.project.description should be( Seq( @@ -316,7 +318,6 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { ) ) received.project.keywords should contain(Iri.fromSparqlEncodedString(keywordWithSpecialCharacter)) - } "return a 'DuplicateValueException' during creation if the supplied project shortname is not unique" in { @@ -358,7 +359,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { } "UPDATE a project" in { - val iri = ITTestDataFactory.projectIri(newProjectIri.get) + val iri = ProjectIri.unsafeFrom(newProjectIri.get) val updatedLongname = Longname.unsafeFrom("updated project longname") val updatedDescription = List( Description.unsafeFrom( @@ -404,7 +405,7 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "return 'NotFound' if a not existing project IRI is submitted during update" in { val longname = Longname.unsafeFrom("longname") - val iri = ITTestDataFactory.projectIri(notExistingProjectButValidProjectIri) + val iri = ProjectIri.unsafeFrom(notExistingProjectButValidProjectIri) appActor ! ProjectChangeRequestADM( projectIri = iri, projectUpdatePayload = ProjectUpdateRequest(longname = Some(longname)), @@ -600,33 +601,29 @@ class ProjectsResponderADMSpec extends CoreSpec with ImplicitSender { "used to query keywords" should { "return all unique keywords for all projects" in { appActor ! ProjectsKeywordsGetRequestADM() + val received: ProjectsKeywordsGetResponseADM = expectMsgType[ProjectsKeywordsGetResponseADM](timeout) received.keywords.size should be(21) } "return all keywords for a single project" in { - val iri = ITTestDataFactory.projectIri(SharedTestDataADM.incunabulaProject.id) - appActor ! ProjectKeywordsGetRequestADM( - projectIri = iri + appActor ! ProjectKeywordsGetRequestADM(projectIri = + ProjectIri.unsafeFrom(SharedTestDataADM.incunabulaProject.id) ) + val received: ProjectKeywordsGetResponseADM = expectMsgType[ProjectKeywordsGetResponseADM](timeout) received.keywords should be(SharedTestDataADM.incunabulaProject.keywords) } "return empty list for a project without keywords" in { - val iri = ITTestDataFactory.projectIri(SharedTestDataADM.dokubibProject.id) - appActor ! ProjectKeywordsGetRequestADM( - projectIri = iri - ) + appActor ! ProjectKeywordsGetRequestADM(ProjectIri.unsafeFrom(SharedTestDataADM.dokubibProject.id)) + val received: ProjectKeywordsGetResponseADM = expectMsgType[ProjectKeywordsGetResponseADM](timeout) received.keywords should be(Seq.empty[String]) } "return 'NotFound' when the project IRI is unknown" in { - val iri = ITTestDataFactory.projectIri(notExistingProjectButValidProjectIri) - appActor ! ProjectKeywordsGetRequestADM( - projectIri = iri - ) + appActor ! ProjectKeywordsGetRequestADM(ProjectIri.unsafeFrom(notExistingProjectButValidProjectIri)) expectMsg(Failure(NotFoundException(s"Project '$notExistingProjectButValidProjectIri' not found."))) } diff --git a/integration/src/test/scala/org/knora/webapi/responders/admin/SipiResponderADMSpec.scala b/integration/src/test/scala/org/knora/webapi/responders/admin/SipiResponderADMSpec.scala index 2b56aeb087..1994b09fae 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/admin/SipiResponderADMSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/admin/SipiResponderADMSpec.scala @@ -5,7 +5,7 @@ package org.knora.webapi.responders.admin -import org.apache.pekko +import org.apache.pekko.testkit._ import scala.concurrent.duration._ @@ -14,8 +14,7 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestric import org.knora.webapi.messages.admin.responder.sipimessages._ import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.sharedtestdata.SharedTestDataADM - -import pekko.testkit._ +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode /** * Tests [[SipiResponderADM]]. @@ -36,7 +35,7 @@ class SipiResponderADMSpec extends CoreSpec with ImplicitSender { "return details of a full quality file value" in { // http://localhost:3333/v1/files/http%3A%2F%2Frdfh.ch%2F8a0b1e75%2Freps%2F7e4ba672 appActor ! SipiFileInfoGetRequestADM( - projectID = "0803", + projectID = Shortcode.unsafeFrom("0803"), filename = "incunabula_0000003328.jp2", requestingUser = SharedTestDataADM.incunabulaMemberUser ) @@ -47,7 +46,7 @@ class SipiResponderADMSpec extends CoreSpec with ImplicitSender { "return details of a restricted view file value" in { // http://localhost:3333/v1/files/http%3A%2F%2Frdfh.ch%2F8a0b1e75%2Freps%2F7e4ba672 appActor ! SipiFileInfoGetRequestADM( - projectID = "0803", + projectID = Shortcode.unsafeFrom("0803"), filename = "incunabula_0000003328.jp2", requestingUser = SharedTestDataADM.anonymousUser ) diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala index 33eaba8ad5..60af9f347d 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/SearchResponderV2Spec.scala @@ -5,31 +5,25 @@ package org.knora.webapi.responders.v2 -import org.apache.pekko - import dsp.errors.BadRequestException -import org.knora.webapi.ApiV2Complex -import org.knora.webapi.CoreSpec -import org.knora.webapi.SchemaOptions +import org.knora.webapi.SchemaAndOptions.apiV2SchemaWithOption +import org.knora.webapi._ import org.knora.webapi.messages.IriConversions._ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.store.triplestoremessages.RdfDataObject import org.knora.webapi.messages.v2.responder.resourcemessages._ -import org.knora.webapi.messages.v2.responder.searchmessages._ import org.knora.webapi.messages.v2.responder.valuemessages.ReadValueV2 import org.knora.webapi.messages.v2.responder.valuemessages.StillImageFileValueContentV2 import org.knora.webapi.responders.v2.ResourcesResponseCheckerV2.compareReadResourcesSequenceV2Response +import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.sharedtestdata.SharedTestDataADM +import org.knora.webapi.sharedtestdata.SharedTestDataADM.anonymousUser +import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA -import pekko.testkit.ImplicitSender - -/** - * Tests [[SearchResponderV2]]. - */ -class SearchResponderV2Spec extends CoreSpec with ImplicitSender { +class SearchResponderV2Spec extends CoreSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - override lazy val rdfDataObjects = List( + override lazy val rdfDataObjects: List[RdfDataObject] = List( RdfDataObject( path = "test_data/project_data/incunabula-data.ttl", name = "http://www.knora.org/data/0803/incunabula" @@ -50,294 +44,249 @@ class SearchResponderV2Spec extends CoreSpec with ImplicitSender { "perform a fulltext search for 'Narr'" in { - appActor ! FulltextSearchRequestV2( - searchValue = "Narr", - offset = 0, - limitToProject = None, - limitToResourceClass = None, - limitToStandoffClass = None, - returnFiles = false, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anonymousUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.fulltextSearchV2( + searchValue = "Narr", + offset = 0, + limitToProject = None, + limitToResourceClass = None, + limitToStandoffClass = None, + returnFiles = false, + apiV2SchemaWithOption(MarkupAsXml), + requestingUser = anonymousUser + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.resources.size == 25) - } + assert(result.resources.size == 25) } "perform a fulltext search for 'Dinge'" in { - - appActor ! FulltextSearchRequestV2( - searchValue = "Dinge", - offset = 0, - limitToProject = None, - limitToResourceClass = None, - limitToStandoffClass = None, - returnFiles = false, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anythingUser1 + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.fulltextSearchV2( + searchValue = "Dinge", + offset = 0, + limitToProject = None, + limitToResourceClass = None, + limitToStandoffClass = None, + returnFiles = false, + apiV2SchemaWithOption(MarkupAsXml), + requestingUser = SharedTestDataADM.anythingUser1 + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.resources.size == 1) - } - + assert(result.resources.size == 1) } "return a Bad Request error if fulltext search input is invalid" in { - - appActor ! FulltextSearchRequestV2( - searchValue = "qin(", - offset = 0, - limitToProject = None, - limitToResourceClass = None, - limitToStandoffClass = None, - returnFiles = false, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anythingUser1 + val result = UnsafeZioRun.run( + SearchResponderV2.fulltextSearchV2( + searchValue = "qin(", + offset = 0, + limitToProject = None, + limitToResourceClass = None, + limitToStandoffClass = None, + returnFiles = false, + apiV2SchemaWithOption(MarkupAsXml), + requestingUser = SharedTestDataADM.anythingUser1 + ) ) - - expectMsgPF(timeout) { case msg: pekko.actor.Status.Failure => - assert(msg.cause.isInstanceOf[BadRequestException]) - } - + assertFailsWithA[BadRequestException](result) } "return files attached to full-text search results" in { - appActor ! FulltextSearchRequestV2( - searchValue = "p7v", - offset = 0, - limitToProject = None, - limitToResourceClass = None, - limitToStandoffClass = None, - returnFiles = true, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anythingUser1 + val result: ReadResourcesSequenceV2 = UnsafeZioRun.runOrThrow( + SearchResponderV2.fulltextSearchV2( + searchValue = "p7v", + offset = 0, + limitToProject = None, + limitToResourceClass = None, + limitToStandoffClass = None, + returnFiles = true, + apiV2SchemaWithOption(MarkupAsXml), + requestingUser = SharedTestDataADM.anythingUser1 + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - val hasImageFileValues: Boolean = - response.resources.flatMap(_.values.values.flatten).exists { readValueV2: ReadValueV2 => - readValueV2.valueContent match { - case _: StillImageFileValueContentV2 => true - case _ => false - } + val hasImageFileValues: Boolean = + result.resources.flatMap(_.values.values.flatten).exists { readValueV2: ReadValueV2 => + readValueV2.valueContent match { + case _: StillImageFileValueContentV2 => true + case _ => false } + } - assert(hasImageFileValues) - } + assert(hasImageFileValues) } "perform an extended search for books that have the title 'Zeitglöcklein des Lebens'" in { - appActor ! GravsearchRequestV2( - constructQuery = searchResponderV2SpecFullData.constructQueryForBooksWithTitleZeitgloecklein, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anonymousUser + val searchResult = UnsafeZioRun.runOrThrow( + SearchResponderV2.gravsearchV2( + searchResponderV2SpecFullData.constructQueryForBooksWithTitleZeitgloecklein, + apiV2SchemaWithOption(MarkupAsXml), + anonymousUser + ) ) // extended search sort by resource Iri by default if no order criterion is indicated - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - compareReadResourcesSequenceV2Response( - expected = searchResponderV2SpecFullData.booksWithTitleZeitgloeckleinResponse, - received = response - ) - } - + compareReadResourcesSequenceV2Response( + expected = searchResponderV2SpecFullData.booksWithTitleZeitgloeckleinResponse, + received = searchResult + ) } "perform an extended search for books that do not have the title 'Zeitglöcklein des Lebens'" in { - - appActor ! GravsearchRequestV2( - constructQuery = searchResponderV2SpecFullData.constructQueryForBooksWithoutTitleZeitgloecklein, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anonymousUser + val searchResult = UnsafeZioRun.runOrThrow( + SearchResponderV2.gravsearchV2( + searchResponderV2SpecFullData.constructQueryForBooksWithoutTitleZeitgloecklein, + apiV2SchemaWithOption(MarkupAsXml), + anonymousUser + ) ) // extended search sort by resource Iri by default if no order criterion is indicated - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - // TODO: do better testing once JSON-LD can be converted back into case classes - assert(response.resources.size == 18) - } - + assert(searchResult.resources.size == 18) } "perform a search by label for incunabula:book that contain 'Narrenschiff'" in { - - appActor ! SearchResourceByLabelRequestV2( - searchValue = "Narrenschiff", - offset = 0, - limitToProject = None, - limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri! - targetSchema = ApiV2Complex, - requestingUser = SharedTestDataADM.anonymousUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.searchResourcesByLabelV2( + searchValue = "Narrenschiff", + offset = 0, + limitToProject = None, + limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri! + targetSchema = ApiV2Complex, + requestingUser = anonymousUser + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.resources.size == 3) - } - + assert(result.resources.size == 3) } "perform a search by label for incunabula:book that contain 'Das Narrenschiff'" in { - - appActor ! SearchResourceByLabelRequestV2( - searchValue = "Narrenschiff", - offset = 0, - limitToProject = None, - limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri! - targetSchema = ApiV2Complex, - requestingUser = SharedTestDataADM.anonymousUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.searchResourcesByLabelV2( + searchValue = "Narrenschiff", + offset = 0, + limitToProject = None, + limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri! + targetSchema = ApiV2Complex, + requestingUser = anonymousUser + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - assert(response.resources.size == 3) - } - + assert(result.resources.size == 3) } "perform a count search query by label for incunabula:book that contain 'Narrenschiff'" in { - appActor ! SearchResourceByLabelCountRequestV2( - searchValue = "Narrenschiff", - limitToProject = None, - limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri! - requestingUser = SharedTestDataADM.anonymousUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.searchResourcesByLabelCountV2( + searchValue = "Narrenschiff", + limitToProject = None, + limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri) + ) ) - expectMsgPF(timeout) { case response: ResourceCountV2 => - assert(response.numberOfResources == 3) - } + assert(result.numberOfResources == 3) } "perform a a count search query by label for incunabula:book that contain 'Passio sancti Meynrhadi martyris et heremite'" in { - appActor ! SearchResourceByLabelCountRequestV2( - searchValue = "Passio sancti Meynrhadi martyris et heremite", - limitToProject = None, - limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri), // internal Iri! - requestingUser = SharedTestDataADM.anonymousUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.searchResourcesByLabelCountV2( + searchValue = "Passio sancti Meynrhadi martyris et heremite", + limitToProject = None, + limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri) + ) ) - expectMsgPF(timeout) { case response: ResourceCountV2 => - assert(response.numberOfResources == 1) - } - + assert(result.numberOfResources == 1) } "search by project and resource class" in { - appActor ! SearchResourcesByProjectAndClassRequestV2( - projectIri = SharedTestDataADM.incunabulaProject.id.toSmartIri, - resourceClass = "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book".toSmartIri, - orderByProperty = Some("http://0.0.0.0:3333/ontology/0803/incunabula/v2#title".toSmartIri), - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - page = 0, - targetSchema = ApiV2Complex, - requestingUser = SharedTestDataADM.incunabulaProjectAdminUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.searchResourcesByProjectAndClassV2( + projectIri = SharedTestDataADM.incunabulaProject.id.toSmartIri, + resourceClass = "http://0.0.0.0:3333/ontology/0803/incunabula/v2#book".toSmartIri, + orderByProperty = Some("http://0.0.0.0:3333/ontology/0803/incunabula/v2#title".toSmartIri), + page = 0, + schemaAndOptions = SchemaAndOptions.apiV2SchemaWithOption(MarkupAsXml), + requestingUser = SharedTestDataADM.incunabulaProjectAdminUser + ) ) - - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - response.resources.size should ===(19) - } + result.resources.size should ===(19) } "search for list label" in { - appActor ! FulltextSearchRequestV2( - searchValue = "non fiction", - offset = 0, - limitToProject = None, - limitToResourceClass = None, - limitToStandoffClass = None, - returnFiles = false, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anythingUser1 + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.fulltextSearchV2( + searchValue = "non fiction", + offset = 0, + limitToProject = None, + limitToResourceClass = None, + limitToStandoffClass = None, + returnFiles = false, + apiV2SchemaWithOption(MarkupAsXml), + requestingUser = SharedTestDataADM.anythingUser1 + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - compareReadResourcesSequenceV2Response( - expected = searchResponderV2SpecFullData.expectedResultFulltextSearchForListNodeLabel, - received = response - ) - } + compareReadResourcesSequenceV2Response( + expected = searchResponderV2SpecFullData.expectedResultFulltextSearchForListNodeLabel, + received = result + ) } "search for list label and find sub-nodes" in { - - appActor ! FulltextSearchRequestV2( - searchValue = "novel", - offset = 0, - limitToProject = None, - limitToResourceClass = None, - limitToStandoffClass = None, - returnFiles = false, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anythingUser1 + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.fulltextSearchV2( + searchValue = "novel", + offset = 0, + limitToProject = None, + limitToResourceClass = None, + limitToStandoffClass = None, + returnFiles = false, + apiV2SchemaWithOption(MarkupAsXml), + requestingUser = SharedTestDataADM.anythingUser1 + ) ) - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - compareReadResourcesSequenceV2Response( - expected = searchResponderV2SpecFullData.expectedResultFulltextSearchForListNodeLabelWithSubnodes, - received = response - ) - } + compareReadResourcesSequenceV2Response( + expected = searchResponderV2SpecFullData.expectedResultFulltextSearchForListNodeLabelWithSubnodes, + received = result + ) } "perform an extended search for a particular compound object (book)" in { - - val query = searchResponderV2SpecFullData.constructQueryForIncunabulaCompundObject - - appActor ! GravsearchRequestV2( - constructQuery = query, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anonymousUser + val searchResult = UnsafeZioRun.runOrThrow( + SearchResponderV2.gravsearchV2( + searchResponderV2SpecFullData.constructQueryForIncunabulaCompundObject, + apiV2SchemaWithOption(MarkupAsXml), + anonymousUser + ) ) - - expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => - response.resources.length should equal(25) - } + searchResult.resources.length should equal(25) } - "perform an extended search ordered by label" in { - + "perform an extended search ordered asc by label" in { val queryAsc = searchResponderV2SpecFullData.constructQuerySortByLabel - - appActor ! GravsearchRequestV2( - constructQuery = queryAsc, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anonymousUser + val ascResult = UnsafeZioRun.runOrThrow( + SearchResponderV2.gravsearchV2(queryAsc, apiV2SchemaWithOption(MarkupAsXml), anonymousUser) ) + assert(ascResult.resources.head.label == "A blue thing") + } - val asc = expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => response } - assert(asc.resources.head.label == "A blue thing") - + "perform an extended search ordered desc by label" in { val queryDesc = searchResponderV2SpecFullData.constructQuerySortByLabelDesc - - appActor ! GravsearchRequestV2( - constructQuery = queryDesc, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = SharedTestDataADM.anonymousUser + val descResult = UnsafeZioRun.runOrThrow( + SearchResponderV2.gravsearchV2(queryDesc, apiV2SchemaWithOption(MarkupAsXml), anonymousUser) ) - - val desc = expectMsgPF(timeout) { case response: ReadResourcesSequenceV2 => response } - assert(desc.resources.head.label == "visible thing with hidden int values") + assert(descResult.resources.head.label == "visible thing with hidden int values") } - } - } diff --git a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala index 29b6afae2c..b089d45772 100644 --- a/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala +++ b/integration/src/test/scala/org/knora/webapi/responders/v2/ValuesResponderV2Spec.scala @@ -27,7 +27,6 @@ import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.messages.util.PermissionUtilADM import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser import org.knora.webapi.messages.v2.responder.resourcemessages._ -import org.knora.webapi.messages.v2.responder.searchmessages.GravsearchRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages._ import org.knora.webapi.messages.v2.responder.valuemessages._ import org.knora.webapi.models.filemodels.FileModelUtil @@ -198,19 +197,15 @@ class ValuesResponderV2Spec extends CoreSpec with ImplicitSender { .toString() // Run the query. - - val parsedGravsearchQuery = GravsearchParser.parseQuery(gravsearchQuery) - - appActor ! GravsearchRequestV2( - constructQuery = parsedGravsearchQuery, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = requestingUser + val result = UnsafeZioRun.runOrThrow( + SearchResponderV2.gravsearchV2( + GravsearchParser.parseQuery(gravsearchQuery), + SchemaAndOptions.apiV2SchemaWithOption(MarkupAsXml), + requestingUser + ) ) - expectMsgPF(timeout) { case searchResponse: ReadResourcesSequenceV2 => - searchResponse.toResource(resourceIri).toOntologySchema(ApiV2Complex) - } + result.toResource(resourceIri).toOntologySchema(ApiV2Complex) } private def getValuesFromResource(resource: ReadResourceV2, propertyIriInResult: SmartIri): Seq[ReadValueV2] = diff --git a/integration/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala b/integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala similarity index 80% rename from integration/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala rename to integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala index 61fb7252d9..503f814ae3 100644 --- a/integration/src/test/scala/org/knora/webapi/store/iiif/impl/IIIFServiceMockImpl.scala +++ b/integration/src/test/scala/org/knora/webapi/store/iiif/impl/SipiServiceMock.scala @@ -12,29 +12,30 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.sipimessages._ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.slice.admin.domain.service.Asset -import org.knora.webapi.store.iiif.api.IIIFService +import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse +import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.store.iiif.errors.SipiException /** - * Can be used in place of [[IIIFServiceSipiImpl]] for tests without an actual Sipi server, by returning hard-coded + * Can be used in place of [[SipiServiceLive]] for tests without an actual Sipi server, by returning hard-coded * responses simulating responses from Sipi. */ -case class IIIFServiceMockImpl() extends IIIFService { +case class SipiServiceMock() extends SipiService { /** * A request with this filename will always cause a Sipi error. */ private val FAILURE_FILENAME: String = "failure.jp2" - def getFileMetadata(getFileMetadataRequestV2: GetFileMetadataRequest): Task[GetFileMetadataResponse] = + override def getFileMetadata(ignoredByMock: String): Task[FileMetadataSipiResponse] = ZIO.succeed( - GetFileMetadataResponse( + FileMetadataSipiResponse( originalFilename = Some("test2.tiff"), originalMimeType = Some("image/tiff"), internalMimeType = "image/jp2", width = Some(512), height = Some(256), - pageCount = None, + numpages = None, duration = None, fps = None ) @@ -63,11 +64,11 @@ case class IIIFServiceMockImpl() extends IIIFService { override def downloadAsset(asset: Asset, targetDir: Path, user: UserADM): Task[Option[Path]] = ??? } -object IIIFServiceMockImpl { +object SipiServiceMock { - val layer: ZLayer[Any, Nothing, IIIFService] = + val layer: ZLayer[Any, Nothing, SipiService] = ZLayer - .succeed(IIIFServiceMockImpl()) + .succeed(SipiServiceMock()) .tap(_ => ZIO.logInfo(">>> Mock Sipi IIIF Service Initialized <<<")) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e6aba4efb0..dd41fc1468 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -13,7 +13,7 @@ object Dependencies { val fusekiImage = "daschswiss/apache-jena-fuseki:2.1.2" // should be the same version as in docker-compose.yml, also make sure to use the same version when deploying it (i.e. version in ops-deploy)! - val sipiImage = "daschswiss/sipi:3.8.3" // base image the knora-sipi image is created from + val sipiImage = "daschswiss/sipi:3.8.6" // base image the knora-sipi image is created from val ScalaVersion = "2.13.12" @@ -22,12 +22,12 @@ object Dependencies { val JenaVersion = "4.8.0" val ZioConfigVersion = "3.0.7" - val ZioLoggingVersion = "2.1.14" + val ZioLoggingVersion = "2.1.15" val ZioNioVersion = "2.0.2" val ZioMetricsConnectorsVersion = "2.2.1" val ZioPreludeVersion = "1.0.0-RC21" val ZioSchemaVersion = "0.2.0" - val ZioVersion = "2.0.18" + val ZioVersion = "2.0.19" // ZIO - all Scala 3 compatible val zio = "dev.zio" %% "zio" % ZioVersion @@ -40,7 +40,7 @@ object Dependencies { val zioNio = "dev.zio" %% "zio-nio" % ZioNioVersion val zioMacros = "dev.zio" %% "zio-macros" % ZioVersion val zioPrelude = "dev.zio" %% "zio-prelude" % ZioPreludeVersion - val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.0" + val zioSttp = "com.softwaremill.sttp.client3" %% "zio" % "3.9.1" // refined val refined = Seq( @@ -70,8 +70,8 @@ object Dependencies { // Metrics val aspectjweaver = "org.aspectj" % "aspectjweaver" % "1.9.20.1" - val kamonCore = "io.kamon" %% "kamon-core" % "2.6.5" // Scala 3 compatible - val kamonScalaFuture = "io.kamon" %% "kamon-scala-future" % "2.6.5" // Scala 3 incompatible + val kamonCore = "io.kamon" %% "kamon-core" % "2.6.6" // Scala 3 compatible + val kamonScalaFuture = "io.kamon" %% "kamon-scala-future" % "2.6.6" // Scala 3 incompatible // input validation val commonsValidator = @@ -96,11 +96,11 @@ object Dependencies { val icu4j = "com.ibm.icu" % "icu4j" % "74.1" val jakartaJSON = "org.glassfish" % "jakarta.json" % "2.0.1" val jodd = "org.jodd" % "jodd" % "3.2.7" - val rdf4jClient = "org.eclipse.rdf4j" % "rdf4j-client" % "4.3.7" - val rdf4jShacl = "org.eclipse.rdf4j" % "rdf4j-shacl" % "4.3.7" + val rdf4jClient = "org.eclipse.rdf4j" % "rdf4j-client" % "4.3.8" + val rdf4jShacl = "org.eclipse.rdf4j" % "rdf4j-shacl" % "4.3.8" val saxonHE = "net.sf.saxon" % "Saxon-HE" % "12.3" val scalaGraph = "org.scala-graph" %% "graph-core" % "1.13.6" // Scala 3 incompatible - val scallop = "org.rogach" %% "scallop" % "5.0.0" // Scala 3 compatible + val scallop = "org.rogach" %% "scallop" % "5.0.1" // Scala 3 compatible val titaniumJSONLD = "com.apicatalog" % "titanium-json-ld" % "1.3.2" val xmlunitCore = "org.xmlunit" % "xmlunit-core" % "2.9.1" @@ -121,7 +121,7 @@ object Dependencies { // found/added by the plugin but deleted anyway val commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.13.0" - val tapirVersion = "1.8.4" + val tapirVersion = "1.8.5" val tapir = Seq( "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion, diff --git a/project/plugins.sbt b/project/plugins.sbt index 94d54a6f97..f06cf54f49 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,11 +5,11 @@ resolvers ++= Seq( // please don't remove or merge uncommented to main //addDependencyTreePlugin -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") -addSbtPlugin("io.kamon" % "sbt-aspectj-runner" % "1.1.2") -addSbtPlugin("com.typesafe.play" % "sbt-twirl" % "1.6.2") -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.4") -addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8") +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") +addSbtPlugin("io.kamon" % "sbt-aspectj-runner" % "1.1.2") +addSbtPlugin("org.playframework.twirl" % "sbt-twirl" % "2.0.1") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.4") +addSbtPlugin("com.github.sbt" % "sbt-javaagent" % "0.1.8") // also update the scalac-scoverage-runtime version in build.sbt addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") diff --git a/sipi/scripts/clean_temp_dir.sh b/sipi/scripts/clean_temp_dir.sh deleted file mode 100644 index 62acc9f43f..0000000000 --- a/sipi/scripts/clean_temp_dir.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -error_msg() { - echo "$(date): failed cleaning temp dir" >> "$log_file" -} -trap error_msg ERR - -# Clear log -log_file="/var/log/cleanTempDir.log" -> "$log_file" - -echo "$(date): calling clean_temp_dir route" >> "$log_file" - -# Call route -curl -u "${CLEAN_TMP_DIR_USER}:${CLEAN_TMP_DIR_PW}" -sS -L --fail 'http://localhost:1024/clean_temp_dir' >> "$log_file" 2>&1 -if [ $? -ne 0 ]; then - echo "$(date): route returned an error status" >> "$log_file" - exit 1 -fi - -echo "$(date): successfully called clean_temp_dir route" >> "$log_file" diff --git a/sipi/scripts/entrypoint.sh b/sipi/scripts/entrypoint.sh deleted file mode 100644 index ca98f80302..0000000000 --- a/sipi/scripts/entrypoint.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -set -o pipefail - -if [ -n "$CLEAN_TMP_DIR_CRON_SCHEDULE" ]; then - parts=$(echo "$CLEAN_TMP_DIR_CRON_SCHEDULE" | wc -w) - command="/bin/bash /sipi/scripts/clean_temp_dir.sh" - - # Validate and install clean temp dir crontab - [ "$parts" -eq 5 ] && (crontab -l 2>/dev/null; echo "$CLEAN_TMP_DIR_CRON_SCHEDULE $command") | crontab - 2>/dev/null - if [ $? -ne 0 ]; then - echo "Invalid clean temp dir cron schedule: $CLEAN_TMP_DIR_CRON_SCHEDULE" >&2 - exit 1 - fi - - # Start cron process in background - cron & -fi - -# Start SIPI -cd /sipi && ./sipi "$@" diff --git a/sipi/scripts/healthcheck.sh b/sipi/scripts/healthcheck.sh index 3ab5b4dd83..7808da194c 100755 --- a/sipi/scripts/healthcheck.sh +++ b/sipi/scripts/healthcheck.sh @@ -1,13 +1,5 @@ #!/bin/bash -if [ -n "$CLEAN_TMP_DIR_CRON_SCHEDULE" ]; then - pgrep -x "cron" >/dev/null - if [ $? -ne 0 ]; then - echo "Cron is not running" - exit 1 - fi -fi - curl -sS --fail 'http://localhost:1024/server/test.html' if [ $? -ne 0 ]; then echo "SIPI did not respond to /server/test.html route" diff --git a/webapi/src/main/scala/dsp/valueobjects/Iri.scala b/webapi/src/main/scala/dsp/valueobjects/Iri.scala index d01825550b..f0be8970ad 100644 --- a/webapi/src/main/scala/dsp/valueobjects/Iri.scala +++ b/webapi/src/main/scala/dsp/valueobjects/Iri.scala @@ -8,7 +8,6 @@ package dsp.valueobjects import com.google.gwt.safehtml.shared.UriUtils.encodeAllowEscapes import org.apache.commons.lang3.StringUtils import org.apache.commons.validator.routines.UrlValidator -import zio.json.JsonCodec import zio.json.JsonDecoder import zio.json.JsonEncoder import zio.prelude.Validation @@ -18,9 +17,10 @@ import scala.util.Try import dsp.errors.BadRequestException import dsp.errors.ValidationException -sealed trait Iri { +trait Iri { val value: String } + object Iri { type IRI = String @@ -229,40 +229,6 @@ object Iri { } } - /** - * ProjectIri value object. - */ - sealed abstract case class ProjectIri private (value: String) extends Iri - object ProjectIri { self => - - implicit val codec: JsonCodec[ProjectIri] = new JsonCodec[ProjectIri]( - JsonEncoder[String].contramap(_.value), - JsonDecoder[String].mapOrFail(ProjectIri.make(_).toEitherWith(e => e.head.getMessage)) - ) - - def make(value: String): Validation[ValidationException, ProjectIri] = - if (value.isEmpty) Validation.fail(ValidationException(IriErrorMessages.ProjectIriMissing)) - else { - val isUuid: Boolean = UuidUtil.hasValidLength(value.split("/").last) - - if (!isProjectIri(value)) - Validation.fail(ValidationException(IriErrorMessages.ProjectIriInvalid)) - else if (isUuid && !UuidUtil.hasSupportedVersion(value)) - Validation.fail(ValidationException(IriErrorMessages.UuidVersionInvalid)) - else - Validation - .fromOption(validateAndEscapeProjectIri(value)) - .mapError(_ => ValidationException(IriErrorMessages.ProjectIriInvalid)) - .map(new ProjectIri(_) {}) - } - - def make(value: Option[String]): Validation[ValidationException, Option[ProjectIri]] = - value match { - case Some(v) => self.make(v).map(Some(_)) - case None => Validation.succeed(None) - } - } - /** * Base64Uuid value object. * This is base64 encoded UUID version without paddings. diff --git a/webapi/src/main/scala/dsp/valueobjects/UuidUtil.scala b/webapi/src/main/scala/dsp/valueobjects/UuidUtil.scala index aa4292635e..dd5f6f3e39 100644 --- a/webapi/src/main/scala/dsp/valueobjects/UuidUtil.scala +++ b/webapi/src/main/scala/dsp/valueobjects/UuidUtil.scala @@ -25,10 +25,7 @@ object UuidUtil { * * @return a random, Base64-encoded UUID. */ - def makeRandomBase64EncodedUuid: String = { - val uuid = UUID.randomUUID - base64Encode(uuid) - } + def makeRandomBase64EncodedUuid: String = base64Encode(UUID.randomUUID) /** * Base64-encodes a [[UUID]] using a URL and filename safe Base64 encoder from [[java.util.Base64]], @@ -101,7 +98,6 @@ object UuidUtil { /** * Calls `base64Decode`, throwing [[InconsistentRepositoryDataException]] if the string cannot be parsed. */ - @deprecated("It is still throwing!") def decode(uuidStr: String): UUID = if (uuidStr.length == canonicalUuidLength) UUID.fromString(uuidStr) else if (uuidStr.length == base64UuidLength) @@ -111,31 +107,4 @@ object UuidUtil { .getOrElse(throw InconsistentRepositoryDataException(s"Invalid UUID: $uuidStr")) else throw InconsistentRepositoryDataException(s"Invalid UUID: $uuidStr") - /** - * Encodes a [[UUID]] as a string in one of two formats: - * - * - The canonical 36-character format. - * - The 22-character Base64-encoded format returned by [[base64Encode]]. - * - * @param uuid the UUID to be encoded. - * @param useBase64 if `true`, uses Base64 encoding. - * @return the encoded UUID. - */ - def encode(uuid: UUID, useBase64: Boolean): String = - if (useBase64) base64Encode(uuid) - else uuid.toString - - /** - * Validates and decodes a Base64-encoded UUID. - * - * @param base64Uuid the UUID to be validated. - * @param errorFun a function that throws an exception. It will be called if the string cannot be parsed. - * @return the decoded UUID. - */ - @deprecated("Use validateBase64EncodedUuid(String) instead.") - def validateBase64EncodedUuid(base64Uuid: String, errorFun: => Nothing): UUID = // V2 / value objects - validateBase64EncodedUuid(base64Uuid).getOrElse(errorFun) - - def validateBase64EncodedUuid(base64Uuid: String): Option[UUID] = - UuidUtil.base64Decode(base64Uuid).toOption } diff --git a/webapi/src/main/scala/org/knora/webapi/OntologySchema.scala b/webapi/src/main/scala/org/knora/webapi/OntologySchema.scala index 8c3372d5c9..7ff3909dca 100644 --- a/webapi/src/main/scala/org/knora/webapi/OntologySchema.scala +++ b/webapi/src/main/scala/org/knora/webapi/OntologySchema.scala @@ -5,6 +5,18 @@ package org.knora.webapi +/** + * Indicates the schema that a Knora ontology or ontology entity conforms to + * and its options that can be submitted to configure an ontology schema. + */ +case class SchemaAndOptions[S <: OntologySchema, O <: SchemaOption](schema: S, options: Set[O]) +object SchemaAndOptions { + def apiV2SchemaWithOption[O <: SchemaOption](option: O): SchemaAndOptions[ApiV2Schema, O] = + apiV2SchemaWithOptions(Set(option)) + def apiV2SchemaWithOptions[O <: SchemaOption](options: Set[O]): SchemaAndOptions[ApiV2Schema, O] = + SchemaAndOptions[ApiV2Schema, O](ApiV2Complex, options) +} + /** * Indicates the schema that a Knora ontology or ontology entity conforms to. */ @@ -85,11 +97,6 @@ object SchemaOptions { */ val ForStandoffWithTextValues: Set[SchemaOption] = Set(MarkupAsXml) - /** - * A set of schema options for querying standoff markup separately from text values. - */ - val ForStandoffSeparateFromTextValues: Set[SchemaOption] = Set(MarkupAsStandoff) - /** * Determines whether standoff should be queried when a text value is queried. * diff --git a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala index 0971d03519..0a8a9903c9 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/AppServer.scala @@ -16,7 +16,7 @@ import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusOK import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.slice.ontology.repo.service.OntologyCache import org.knora.webapi.store.cache.api.CacheService -import org.knora.webapi.store.iiif.api.IIIFService +import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.domain.TriplestoreStatus import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater @@ -31,7 +31,7 @@ final case class AppServer( ru: RepositoryUpdater, as: ActorSystem, ontologyCache: OntologyCache, - iiifs: IIIFService, + sipiService: SipiService, cs: CacheService, hs: HttpServer, appConfig: AppConfig @@ -99,7 +99,7 @@ final case class AppServer( private def checkIIIFService(requiresIIIFService: Boolean): UIO[Unit] = for { _ <- state.set(AppState.WaitingForIIIFService) - _ <- iiifs + _ <- sipiService .getStatus() .flatMap { case IIIFServiceStatusOK => @@ -156,7 +156,7 @@ final case class AppServer( object AppServer { private type AppServerEnvironment = - State & TriplestoreService & RepositoryUpdater & ActorSystem & OntologyCache & IIIFService & CacheService & + State & TriplestoreService & RepositoryUpdater & ActorSystem & OntologyCache & SipiService & CacheService & HttpServer & AppConfig /** @@ -169,7 +169,7 @@ object AppServer { ru <- ZIO.service[RepositoryUpdater] as <- ZIO.service[ActorSystem] oc <- ZIO.service[OntologyCache] - iiifs <- ZIO.service[IIIFService] + iiifs <- ZIO.service[SipiService] cs <- ZIO.service[CacheService] hs <- ZIO.service[HttpServer] c <- ZIO.service[AppConfig] diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index e08b971d67..b455df8871 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -53,8 +53,8 @@ import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl import org.knora.webapi.store.iiif.IIIFRequestMessageHandler import org.knora.webapi.store.iiif.IIIFRequestMessageHandlerLive -import org.knora.webapi.store.iiif.api.IIIFService -import org.knora.webapi.store.iiif.impl.IIIFServiceSipiImpl +import org.knora.webapi.store.iiif.api.SipiService +import org.knora.webapi.store.iiif.impl.SipiServiceLive import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.impl.TriplestoreServiceLive import org.knora.webapi.store.triplestore.upgrade.RepositoryUpdater @@ -68,7 +68,7 @@ object LayersLive { ActorSystem & ApiRoutes & AppConfigurations & AppRouter & Authenticator & CacheService & CacheServiceRequestMessageHandler & CardinalityHandler & CardinalityService & ConstructResponseUtilV2 & ConstructTransformer & GravsearchTypeInspectionRunner & GroupsResponderADM & HttpServer & - IIIFRequestMessageHandler & IIIFService & InferenceOptimizationService & IriService & IriConverter & JwtService & + IIIFRequestMessageHandler & SipiService & InferenceOptimizationService & IriService & IriConverter & JwtService & KnoraProjectRepo & ListsResponderADM & ListsResponderV2 & MessageRelay & OntologyCache & OntologyHelpers & OntologyRepo & OntologyResponderV2 & PermissionUtilADM & PermissionsResponderADM & PredicateObjectMapper & ProjectADMRestService & ProjectADMService & ProjectExportService & ProjectExportStorageService & @@ -101,7 +101,7 @@ object LayersLive { HandlerMapper.layer, HttpServer.layer, IIIFRequestMessageHandlerLive.layer, - IIIFServiceSipiImpl.layer, + SipiServiceLive.layer, InferenceOptimizationService.layer, IriConverter.layer, IriService.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala index b47c3c832b..ded490da6d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala @@ -25,16 +25,16 @@ import dsp.valueobjects.UuidUtil import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.messages.StringFormatter.* +import org.knora.webapi.messages.XmlPatterns.nCNamePattern +import org.knora.webapi.messages.XmlPatterns.nCNameRegex import org.knora.webapi.messages.store.triplestoremessages.StringLiteralSequenceV2 import org.knora.webapi.messages.store.triplestoremessages.StringLiteralV2 import org.knora.webapi.messages.v2.responder.KnoraContentV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.util.Base64UrlCheckDigit import org.knora.webapi.util.JavaUtil -import XmlPatterns.nCNamePattern -import XmlPatterns.nCNameRegex - /** * Provides instances of [[StringFormatter]], as well as string formatting constants. */ @@ -875,13 +875,13 @@ class StringFormatter private ( (true, Some(DefaultSharedOntologiesProjectCode)) } else if (ontologyPath.length == 3) { // other shared ontologies project - (true, Some(validateProjectShortcode(ontologyPath(1), errorFun))) + (true, Some(Try(Shortcode.unsafeFrom(ontologyPath(1)).value).getOrElse(errorFun))) } else { errorFun } } else if (ontologyPath.length == 2) { // non-shared ontology with project code - (false, Some(validateProjectShortcode(ontologyPath.head, errorFun))) + (false, Some(Try(Shortcode.unsafeFrom(ontologyPath.head).value).getOrElse(errorFun))) } else { // built-in ontology (false, None) @@ -1525,19 +1525,6 @@ class StringFormatter private ( case _ => false } - /** - * Given the project shortcode, checks if it is in a valid format, and converts it to upper case. - * - * @param shortcode the project's shortcode. - * @return the shortcode in upper case. - */ - @deprecated("Use def validateProjectShortcode(String) instead.") - def validateProjectShortcode(shortcode: String, errorFun: => Nothing): String = // V2 / value objects - validateProjectShortcode(shortcode).getOrElse(errorFun) - - def validateProjectShortcode(shortcode: String): Option[String] = - ProjectIDRegex.findFirstIn(shortcode.toUpperCase) - /** * Given the group IRI, checks if it is in a valid format. * @@ -1641,14 +1628,6 @@ class StringFormatter private ( } } - /** - * Constructs a path for accessing a file that has been uploaded to Sipi's temporary storage. - * - * @param filename the filename. - * @return a URL for accessing the file. - */ - def makeSipiTempFilePath(filename: String): String = s"/tmp/$filename" - /** * Creates a new resource IRI based on a UUID. * diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsPayloadsADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsPayloadsADM.scala index 909331d06c..634bc8c5e5 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsPayloadsADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsPayloadsADM.scala @@ -7,6 +7,7 @@ package org.knora.webapi.messages.admin.responder.groupsmessages import dsp.valueobjects.Group.* import dsp.valueobjects.Iri.* +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri /** * Group create payload diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala index bc92735f0a..31b3c30a00 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/listsmessages/ListsPayloadsADM.scala @@ -7,6 +7,7 @@ package org.knora.webapi.messages.admin.responder.listsmessages import dsp.valueobjects.Iri.* import dsp.valueobjects.List.* +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri /** * List root node and child node creation payloads diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala index 20ae4e0963..0b85a398d2 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/permissionsmessages/PermissionsMessagesADM.scala @@ -23,7 +23,6 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsADMJso import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.messages.store.triplestoremessages.TriplestoreJsonProtocol import org.knora.webapi.messages.traits.Jsonable -import org.knora.webapi.slice.resourceinfo.domain.InternalIri import pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport @@ -881,7 +880,6 @@ case class PermissionsDataADM( /* Is the user a member of the ProjectAdmin group */ def isProjectAdmin(projectIri: IRI): Boolean = groupsPerProject.getOrElse(projectIri, List.empty[IRI]).contains(OntologyConstants.KnoraAdmin.ProjectAdmin) - def isProjectAdmin(projectIri: InternalIri): Boolean = isProjectAdmin(projectIri.value) /* Does the user have the 'ProjectAdminAllPermission' permission for the project */ def hasProjectAdminAllPermissionFor(projectIri: IRI): Boolean = diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala index 1cdb70a1b5..fbc57db0eb 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/projectsmessages/ProjectsMessagesADM.scala @@ -6,7 +6,7 @@ package org.knora.webapi.messages.admin.responder.projectsmessages import org.apache.commons.lang3.builder.HashCodeBuilder -import org.apache.pekko +import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json.DefaultJsonProtocol import spray.json.JsValue import spray.json.JsonFormat @@ -24,7 +24,6 @@ import dsp.errors.BadRequestException import dsp.errors.OntologyConstraintException import dsp.errors.ValidationException import dsp.valueobjects.Iri -import dsp.valueobjects.Iri.ProjectIri import dsp.valueobjects.RestrictedViewSize import dsp.valueobjects.V2 import org.knora.webapi.IRI @@ -39,8 +38,6 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectC import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest import org.knora.webapi.slice.admin.domain.model.KnoraProject.* -import pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Messages @@ -376,7 +373,7 @@ object ProjectIdentifierADM { ) def fromString(value: String): Validation[ValidationException, IriIdentifier] = - ProjectIri.make(value).map(IriIdentifier(_)) + ProjectIri.from(value).map(IriIdentifier(_)) implicit val tapirCodec: Codec[String, IriIdentifier, TextPlain] = Codec.string.mapDecode(str => diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala index d12cb380ab..9a57817a7d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/sipimessages/SipiMessagesADM.scala @@ -5,7 +5,7 @@ package org.knora.webapi.messages.admin.responder.sipimessages -import org.apache.pekko +import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json.DefaultJsonProtocol import spray.json.JsValue import spray.json.NullOptions @@ -17,8 +17,7 @@ import org.knora.webapi.messages.admin.responder.KnoraResponseADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestrictedViewSettingsADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectsADMJsonProtocol import org.knora.webapi.messages.admin.responder.usersmessages.UserADM - -import pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode /** * An abstract trait representing a Knora v1 API request message that can be sent to `SipiResponderV2`. @@ -33,7 +32,7 @@ sealed trait SipiResponderRequestADM extends KnoraRequestADM with RelayedMessage * @param requestingUser the profile of the user making the request. */ case class SipiFileInfoGetRequestADM( - projectID: String, + projectID: Shortcode, filename: String, requestingUser: UserADM ) extends SipiResponderRequestADM diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala index 4b7c3855a0..6213019e5d 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/sipimessages/SipiMessages.scala @@ -16,43 +16,11 @@ import org.knora.webapi.messages.traits.RequestWithSender import pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport /** - * An abstract trait for messages that can be sent to the [[org.knora.webapi.store.iiif.api.IIIFService]] + * An abstract trait for messages that can be sent to the [[org.knora.webapi.store.iiif.api.SipiService]] */ sealed trait IIIFRequest extends StoreRequest with RelayedMessage -sealed trait SipiRequest extends IIIFRequest { - def requestingUser: UserADM -} - -/** - * Requests file metadata from Sipi. A successful response is a [[GetFileMetadataResponse]]. - * - * @param filePath the path at which Sipi can serve the file. - * @param requestingUser the user making the request. - */ -case class GetFileMetadataRequest(filePath: String, requestingUser: UserADM) extends SipiRequest - -/** - * Represents file metadata returned by Sipi. - * - * @param originalFilename the file's original filename, if known. - * @param originalMimeType the file's original MIME type. - * @param internalMimeType the file's internal MIME type. Always defined (https://dasch.myjetbrains.com/youtrack/issue/DSP-711). - * @param width the file's width in pixels, if applicable. - * @param height the file's height in pixels, if applicable. - * @param pageCount the number of pages in the file, if applicable. - * @param duration the duration of the file in seconds, if applicable. - */ -case class GetFileMetadataResponse( - originalFilename: Option[String], - originalMimeType: Option[String], - internalMimeType: String, - width: Option[Int], - height: Option[Int], - pageCount: Option[Int], - duration: Option[BigDecimal], - fps: Option[BigDecimal] -) +sealed trait SipiRequest extends IIIFRequest /** * Asks Sipi to move a file from temporary to permanent storage. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala b/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala index 4b09724252..24c05680cf 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/store/triplestoremessages/TriplestoreMessages.scala @@ -161,7 +161,9 @@ case class RdfDataObject(path: String, name: String) /** * Represents the subject of a statement read from the triplestore. */ -sealed trait SubjectV2 +sealed trait SubjectV2 { + def value: String +} /** * Represents an IRI used as the subject of a statement. diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/ErrorHandlingMap.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/ErrorHandlingMap.scala index f8bd58ea0e..d47e828156 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/ErrorHandlingMap.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/ErrorHandlingMap.scala @@ -20,7 +20,6 @@ import dsp.errors.InconsistentRepositoryDataException * @tparam A the type of keys in the map. * @tparam B the type of values in the map. */ -@deprecated("Deprecated!") class ErrorHandlingMap[A, B]( toWrap: Map[A, B], private val errorTemplateFun: A => String, diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala index 4eeb8cd724..3a5a5a569f 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/JsonLDUtil.scala @@ -471,7 +471,6 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * @tparam T the type returned by the validation function. * @return the return value of the validation function. */ - @deprecated("Use getIri() instead") @throws[BadRequestException] def toIri[T](validationFun: (String, => Nothing) => T): T = getIri match { @@ -526,7 +525,6 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * @tparam T the type of the validation function's return value. * @return the return value of the validation function. */ - @deprecated("Use getString(String) instead") @throws[BadRequestException] def requireStringWithValidation[T](key: String, validationFun: (String, => Nothing) => T): T = { val str: String = getRequiredString(key).fold(msg => throw BadRequestException(msg), identity) @@ -566,7 +564,6 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * @tparam T the type of the validation function's return value. * @return the return value of the validation function, or `None` if the value was not present. */ - @deprecated("Use getString(String) instead") @throws[BadRequestException] def maybeStringWithValidation[T](key: String, validationFun: (String, => Nothing) => T): Option[T] = getString(key) @@ -582,7 +579,6 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * @param key the key of the required value. * @return the validated IRI. */ - @deprecated("use getIdIriInObject(String) instead") @throws[BadRequestException] def requireIriInObject[T](key: String, validationFun: (String, => Nothing) => T): T = getRequiredObject(key) @@ -602,7 +598,6 @@ case class JsonLDObject(value: Map[String, JsonLDValue]) extends JsonLDValue { * @tparam T the type of the validation function's return value. * @return the return value of the validation function, or `None` if the value was not present. */ - @deprecated("use getIdIriInObject(String) instead") def maybeIriInObject[T](key: String, validationFun: (String, => Nothing) => T): Option[T] = getObject(key) .fold(e => throw BadRequestException(e), identity) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala index a53bb06789..4e6058a98e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/mainquery/GravsearchMainQueryGenerator.scala @@ -14,7 +14,6 @@ import org.knora.webapi.messages.util.ErrorHandlingMap import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.messages.util.search.* -import org.knora.webapi.messages.util.search.gravsearch.prequery.AbstractPrequeryGenerator import org.knora.webapi.messages.util.search.gravsearch.prequery.GravsearchToPrequeryTransformer object GravsearchMainQueryGenerator { @@ -135,7 +134,7 @@ object GravsearchMainQueryGenerator { case Some(depResIri: IRI) => // IRIs are concatenated by GROUP_CONCAT using a separator, split them. // Ignore empty strings, which could result from unbound variables in a UNION. - depResIri.split(AbstractPrequeryGenerator.groupConcatSeparator).toSeq.filter(_.nonEmpty) + depResIri.split(StringFormatter.INFORMATION_SEPARATOR_ONE).toSeq.filter(_.nonEmpty) case None => Set.empty[IRI] // no Iri present since variable was inside aan OPTIONAL or UNION } @@ -192,7 +191,7 @@ object GravsearchMainQueryGenerator { case Some(valObjIris) => // IRIs are concatenated by GROUP_CONCAT using a separator, split them. // Ignore empty strings, which could result from unbound variables in a UNION. - valObjIris.split(AbstractPrequeryGenerator.groupConcatSeparator).toSet.filter(_.nonEmpty) + valObjIris.split(StringFormatter.INFORMATION_SEPARATOR_ONE).toSet.filter(_.nonEmpty) case None => Set.empty[IRI] // since variable was inside aan OPTIONAL or UNION diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala index 352fc28f68..200de9b400 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/AbstractPrequeryGenerator.scala @@ -25,11 +25,6 @@ import org.knora.webapi.messages.util.search.gravsearch.types.* import org.knora.webapi.messages.v2.responder.valuemessages.DateValueContentV2 import org.knora.webapi.util.ApacheLuceneSupport.LuceneQueryString -object AbstractPrequeryGenerator { - // separator used by GroupConcat - val groupConcatSeparator: Char = StringFormatter.INFORMATION_SEPARATOR_ONE -} - /** * An abstract base class for [[WhereTransformer]] instances that generate SPARQL prequeries from Gravsearch input. * diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisation.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisation.scala index 006eb24e08..5991177c2c 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisation.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchQueryOptimisation.scala @@ -345,13 +345,12 @@ private object ReorderPatternsByDependency { * @return the optimised query patterns. */ def reorderPatternsByDependency(patterns: Seq[QueryPattern]): Seq[QueryPattern] = { - val (statementPatterns: Seq[StatementPattern], otherPatterns: Seq[QueryPattern]) = - patterns.partition { - case _: StatementPattern => true - case _ => false - } + val (statementPatterns, otherPatterns) = patterns.partition { + case _: StatementPattern => true + case _ => false + } - val sortedStatementPatterns = createAndSortGraph(statementPatterns) + val sortedStatementPatterns = createAndSortGraph(statementPatterns.asInstanceOf[Seq[StatementPattern]]) val sortedOtherPatterns = otherPatterns.map { case unionPattern: UnionPattern => diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchToPrequeryTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchToPrequeryTransformer.scala index 7b7a0337f1..8865ac0864 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchToPrequeryTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/prequery/GravsearchToPrequeryTransformer.scala @@ -12,8 +12,8 @@ import dsp.errors.AssertionException import dsp.errors.GravsearchException import org.knora.webapi.* import org.knora.webapi.config.AppConfig +import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.util.search.* -import org.knora.webapi.messages.util.search.gravsearch.prequery.AbstractPrequeryGenerator.* import org.knora.webapi.messages.util.search.gravsearch.transformers.SparqlTransformer import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInspectionResult import org.knora.webapi.messages.util.search.gravsearch.types.GravsearchTypeInspectionUtil @@ -190,7 +190,7 @@ class GravsearchToPrequeryTransformer( val groupConcats: Set[GroupConcat] = valueVariables.map { (valueObjVar: QueryVariable) => GroupConcat( inputVariable = valueObjVar, - separator = groupConcatSeparator, + separator = StringFormatter.INFORMATION_SEPARATOR_ONE, outputVariableName = valueObjVar.variableName + groupConcatVariableSuffix ) } @@ -225,7 +225,7 @@ class GravsearchToPrequeryTransformer( (dependentResVar: QueryVariable) => GroupConcat( inputVariable = dependentResVar, - separator = groupConcatSeparator, + separator = StringFormatter.INFORMATION_SEPARATOR_ONE, outputVariableName = dependentResVar.variableName + groupConcatVariableSuffix ) } diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/ConstructTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/ConstructTransformer.scala index 447d114662..e4e9c65bac 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/ConstructTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/ConstructTransformer.scala @@ -38,7 +38,7 @@ final case class ConstructTransformer( optimisedPatterns <- ZIO.attempt( SparqlTransformer.moveBindToBeginning( - SparqlTransformer.optimiseIsDeletedWithMinus( + SparqlTransformer.optimiseIsDeletedWithFilter( SparqlTransformer.moveLuceneToBeginning(patterns) ) ) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SelectTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SelectTransformer.scala index 7d5b658bf1..903ba13665 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SelectTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SelectTransformer.scala @@ -39,7 +39,7 @@ class SelectTransformer( limitInferenceToOntologies = limitInferenceToOntologies ) override def optimiseQueryPatterns(patterns: Seq[QueryPattern]): Task[Seq[QueryPattern]] = ZIO.attempt { - moveBindToBeginning(optimiseIsDeletedWithMinus(moveLuceneToBeginning(patterns))) + moveBindToBeginning(optimiseIsDeletedWithFilter(moveLuceneToBeginning(patterns))) } /** diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformer.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformer.scala index 2f41be4206..fafca73e39 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformer.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/transformers/SparqlTransformer.scala @@ -87,13 +87,13 @@ object SparqlTransformer { createUniqueVariableFromStatement(baseStatement, "LinkValue") /** - * Optimises a query by replacing `knora-base:isDeleted false` with a `MINUS` pattern + * Optimises a query by replacing `knora-base:isDeleted false` with a `FILTER NOT EXISTS` pattern * placed at the end of the block. * * @param patterns the block of patterns to be optimised. * @return the result of the optimisation. */ - def optimiseIsDeletedWithMinus(patterns: Seq[QueryPattern]): Seq[QueryPattern] = { + def optimiseIsDeletedWithFilter(patterns: Seq[QueryPattern]): Seq[QueryPattern] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance // Separate the knora-base:isDeleted statements from the rest of the block. @@ -107,17 +107,18 @@ object SparqlTransformer { case _ => false } - // Replace the knora-base:isDeleted statements with MINUS patterns. - val filterPatterns: Seq[MinusPattern] = isDeletedPatterns.collect { case statementPattern: StatementPattern => - MinusPattern( - Seq( - StatementPattern( - subj = statementPattern.subj, - pred = IriRef(OntologyConstants.KnoraBase.IsDeleted.toSmartIri), - obj = XsdLiteral(value = "true", datatype = OntologyConstants.Xsd.Boolean.toSmartIri) + // Replace the knora-base:isDeleted statements with FILTER NOT EXISTS patterns. + val filterPatterns: Seq[FilterNotExistsPattern] = isDeletedPatterns.collect { + case statementPattern: StatementPattern => + FilterNotExistsPattern( + Seq( + StatementPattern( + subj = statementPattern.subj, + pred = IriRef(OntologyConstants.KnoraBase.IsDeleted.toSmartIri), + obj = XsdLiteral(value = "true", datatype = OntologyConstants.Xsd.Boolean.toSmartIri) + ) ) ) - ) } otherPatterns ++ filterPatterns diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala index dff3ac0314..c2b609496e 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/search/gravsearch/types/GravsearchTypeInspectionRunner.scala @@ -64,26 +64,22 @@ final case class GravsearchTypeInspectionRunner( lastResult <- typeInspectionPipeline(whereClause, initialResult, requestingUser) untypedEntities: Set[TypeableEntity] = lastResult.untypedEntities - _ <- // Are any entities still untyped? - if (untypedEntities.nonEmpty) { - ZIO.fail( - // Yes. Return an error. - GravsearchException( - s"Types could not be determined for one or more entities: ${untypedEntities.mkString(", ")}" - ) - ) - } else { - // No. Are there any entities with multiple types? - val inconsistentEntities: Map[TypeableEntity, Set[GravsearchEntityTypeInfo]] = - lastResult.entitiesWithInconsistentTypes - ZIO.fail { - // Yes. Return an error. - val inconsistentStr = inconsistentEntities.map { case (entity, entityTypes) => - s"$entity ${entityTypes.mkString(" ; ")} ." - }.mkString(" ") - GravsearchException(s"One or more entities have inconsistent types: $inconsistentStr") - }.when(inconsistentEntities.nonEmpty) - } + _ <- ZIO + .fail( + GravsearchException( + s"Types could not be determined for one or more entities: ${untypedEntities.mkString(", ")}" + ) + ) + .when(untypedEntities.nonEmpty) + + inconsistentEntities = lastResult.entitiesWithInconsistentTypes + _ <- ZIO.fail { + val inconsistentStr = inconsistentEntities.map { case (entity, entityTypes) => + s"$entity ${entityTypes.mkString(" ; ")} ." + }.mkString(" ") + GravsearchException(s"One or more entities have inconsistent types: $inconsistentStr") + } + .when(inconsistentEntities.nonEmpty) } yield lastResult.toFinalResult /** diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/XMLToStandoffUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/XMLToStandoffUtil.scala index 495013760d..d706d6dd00 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/XMLToStandoffUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/standoff/XMLToStandoffUtil.scala @@ -957,7 +957,7 @@ class XMLToStandoffUtil( val id = uuidsToDocumentSpecificIds.get(tag.uuid) match { case Some(documentSpecificId) => documentSpecificId - case None => UuidUtil.encode(tag.uuid, writeBase64IDs) + case None => if (writeBase64IDs) UuidUtil.base64Encode(tag.uuid) else tag.uuid.toString } val maybeIdAttr: Option[(String, String)] = if (writeUuidsToXml) { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala index da6caeddd0..e243e0c9ee 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/resourcemessages/ResourceMessagesV2.scala @@ -37,6 +37,7 @@ import org.knora.webapi.messages.v2.responder.* import org.knora.webapi.messages.v2.responder.standoffmessages.MappingXMLtoStandoff import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.util.* /** @@ -673,7 +674,7 @@ object CreateResourceRequestV2 { jsonLDDocument: JsonLDDocument, apiRequestID: UUID, requestingUser: UserADM - ): ZIO[IriConverter & StringFormatter & MessageRelay, Throwable, CreateResourceRequestV2] = + ): ZIO[IriConverter & SipiService & StringFormatter & MessageRelay, Throwable, CreateResourceRequestV2] = ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => val validationFun: (String, => Nothing) => String = (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala deleted file mode 100644 index ca397bf7d7..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/searchmessages/SearchMessagesV2.scala +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.messages.v2.responder.searchmessages - -import org.knora.webapi.ApiV2Schema -import org.knora.webapi.IRI -import org.knora.webapi.SchemaOption -import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.RelayedMessage -import org.knora.webapi.messages.OntologyConstants -import org.knora.webapi.messages.ResponderRequest.KnoraRequestV2 -import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.util.rdf.JsonLDDocument -import org.knora.webapi.messages.util.rdf.JsonLDInt -import org.knora.webapi.messages.util.rdf.JsonLDObject -import org.knora.webapi.messages.util.rdf.JsonLDString -import org.knora.webapi.messages.util.search.ConstructQuery -import org.knora.webapi.messages.v2.responder.* - -/** - * An abstract trait for messages that can be sent to `SearchResponderV2`. - */ -sealed trait SearchResponderRequestV2 extends KnoraRequestV2 with RelayedMessage { - - def requestingUser: UserADM -} - -/** - * Requests the amount of results (resources count) of a given fulltext search. A successful response will be a [[ResourceCountV2]]. - * - * @param searchValue the values to search for. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param requestingUser the user making the request. - */ -case class FullTextSearchCountRequestV2( - searchValue: String, - limitToProject: Option[IRI], - limitToResourceClass: Option[SmartIri], - limitToStandoffClass: Option[SmartIri], - requestingUser: UserADM -) extends SearchResponderRequestV2 - -/** - * Requests a fulltext search. A successful response will be a [[org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourcesSequenceV2]]. - * - * @param searchValue the values to search for. - * @param offset the offset to be used for paging. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param returnFiles if true, return any file value value attached to each matching resource. - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. - * @param requestingUser the user making the request. - */ -case class FulltextSearchRequestV2( - searchValue: String, - offset: Int, - limitToProject: Option[IRI], - limitToResourceClass: Option[SmartIri], - limitToStandoffClass: Option[SmartIri], - returnFiles: Boolean, - targetSchema: ApiV2Schema, - schemaOptions: Set[SchemaOption], - requestingUser: UserADM -) extends SearchResponderRequestV2 - -/** - * Requests the amount of results (resources count) of a given Gravsearch query. A successful response will be a [[ResourceCountV2]]. - * - * @param constructQuery a Sparql construct query provided by the client. - * @param requestingUser the user making the request. - */ -case class GravsearchCountRequestV2( - constructQuery: ConstructQuery, - requestingUser: UserADM -) extends SearchResponderRequestV2 - -/** - * Performs a Gravsearch query. A successful response will be a [[org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourcesSequenceV2]]. - * - * @param constructQuery a Sparql construct query provided by the client. - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. - * @param requestingUser the user making the request. - */ -case class GravsearchRequestV2( - constructQuery: ConstructQuery, - targetSchema: ApiV2Schema, - schemaOptions: Set[SchemaOption] = Set.empty[SchemaOption], - requestingUser: UserADM -) extends SearchResponderRequestV2 - -/** - * Requests a search of resources by their label. A successful response will be a [[ResourceCountV2]]. - * - * @param searchValue the values to search for. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param requestingUser the user making the request. - */ -case class SearchResourceByLabelCountRequestV2( - searchValue: String, - limitToProject: Option[IRI], - limitToResourceClass: Option[SmartIri], - requestingUser: UserADM -) extends SearchResponderRequestV2 - -/** - * Requests a search of resources by their label. A successful response will be a [[org.knora.webapi.messages.v2.responder.resourcemessages.ReadResourcesSequenceV2]]. - * - * @param searchValue the values to search for. - * @param offset the offset to be used for paging. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param targetSchema the schema of the response. - * @param requestingUser the user making the request. - */ -case class SearchResourceByLabelRequestV2( - searchValue: String, - offset: Int, - limitToProject: Option[IRI], - limitToResourceClass: Option[SmartIri], - targetSchema: ApiV2Schema, - requestingUser: UserADM -) extends SearchResponderRequestV2 - -/** - * Represents the number of resources found by a search query. - */ -case class ResourceCountV2(numberOfResources: Int) extends KnoraJsonLDResponseV2 { - override def toJsonLDDocument( - targetSchema: ApiV2Schema, - appConfig: AppConfig, - schemaOptions: Set[SchemaOption] - ): JsonLDDocument = - JsonLDDocument( - body = JsonLDObject( - Map( - OntologyConstants.SchemaOrg.NumberOfItems -> JsonLDInt(numberOfResources) - ) - ), - context = JsonLDObject( - Map( - "schema" -> JsonLDString(OntologyConstants.SchemaOrg.SchemaOrgPrefixExpansion) - ) - ) - ) -} - -/** - * Requests resources of the specified class from the specified project. - * - * @param projectIri the IRI of the project. - * @param resourceClass the IRI of the resource class, in the complex schema. - * @param orderByProperty the IRI of the property that the resources are to be ordered by, in the complex schema. - * @param page the page number of the results page to be returned. - * @param targetSchema the schema of the response. - * @param schemaOptions the schema options submitted with the request. - * @param requestingUser the user making the request. - */ -case class SearchResourcesByProjectAndClassRequestV2( - projectIri: SmartIri, - resourceClass: SmartIri, - orderByProperty: Option[SmartIri], - page: Int, - targetSchema: ApiV2Schema, - schemaOptions: Set[SchemaOption], - requestingUser: UserADM -) extends SearchResponderRequestV2 diff --git a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala index ac232b6ae4..9435f542f1 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/v2/responder/valuemessages/ValueMessagesV2.scala @@ -30,8 +30,6 @@ import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.store.sipimessages.GetFileMetadataRequest -import org.knora.webapi.messages.store.sipimessages.GetFileMetadataResponse import org.knora.webapi.messages.util.PermissionUtilADM.EntityPermission import org.knora.webapi.messages.util.* import org.knora.webapi.messages.util.rdf.* @@ -44,6 +42,8 @@ import org.knora.webapi.messages.v2.responder.standoffmessages.* import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilZ import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse +import org.knora.webapi.store.iiif.api.SipiService /** * A tagging trait for requests handled by [[org.knora.webapi.responders.v2.ValuesResponderV2]]. @@ -619,7 +619,7 @@ object CreateValueV2 { def fromJsonLd( jsonLdString: String, requestingUser: UserADM - ): ZIO[StringFormatter & IriConverter & MessageRelay, Throwable, CreateValueV2] = + ): ZIO[SipiService & StringFormatter & IriConverter & MessageRelay, Throwable, CreateValueV2] = ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => for { // Get the IRI of the resource that the value is to be created in. @@ -740,7 +740,7 @@ object UpdateValueV2 { def fromJsonLd( jsonLdString: String, requestingUser: UserADM - ): ZIO[IriConverter & StringFormatter & MessageRelay, Throwable, UpdateValueV2] = + ): ZIO[IriConverter & SipiService & StringFormatter & MessageRelay, Throwable, UpdateValueV2] = ZIO.serviceWithZIO[StringFormatter] { implicit stringFormatter => def makeUpdateValueContentV2( resourceIri: SmartIri, @@ -1048,8 +1048,8 @@ object ValueContentV2 { def fromJsonLdObject( jsonLdObject: JsonLDObject, requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, ValueContentV2] = ZIO.serviceWithZIO[StringFormatter] { - stringFormatter => + ): ZIO[SipiService & StringFormatter & MessageRelay, Throwable, ValueContentV2] = + ZIO.serviceWithZIO[StringFormatter] { stringFormatter => for { valueType <- ZIO.attempt(jsonLdObject.requireStringWithValidation(JsonLDKeywords.TYPE, stringFormatter.toSmartIriWithErr)) @@ -1069,17 +1069,17 @@ object ValueContentV2 { case UriValue => UriValueContentV2.fromJsonLdObject(jsonLdObject) case GeonameValue => GeonameValueContentV2.fromJsonLdObject(jsonLdObject) case ColorValue => ColorValueContentV2.fromJsonLdObject(jsonLdObject) - case StillImageFileValue => StillImageFileValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) - case DocumentFileValue => DocumentFileValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) - case TextFileValue => TextFileValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) - case AudioFileValue => AudioFileValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) - case MovingImageFileValue => MovingImageFileValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) - case ArchiveFileValue => ArchiveFileValueContentV2.fromJsonLdObject(jsonLdObject, requestingUser) + case StillImageFileValue => StillImageFileValueContentV2.fromJsonLdObject(jsonLdObject) + case DocumentFileValue => DocumentFileValueContentV2.fromJsonLdObject(jsonLdObject) + case TextFileValue => TextFileValueContentV2.fromJsonLdObject(jsonLdObject) + case AudioFileValue => AudioFileValueContentV2.fromJsonLdObject(jsonLdObject) + case MovingImageFileValue => MovingImageFileValueContentV2.fromJsonLdObject(jsonLdObject) + case ArchiveFileValue => ArchiveFileValueContentV2.fromJsonLdObject(jsonLdObject) case other => ZIO.fail(NotImplementedException(s"Parsing of JSON-LD value type not implemented: $other")) } } yield valueContent - } + } } /** @@ -2571,37 +2571,25 @@ case class FileValueV2( * @param fileValue a [[FileValueV2]]. * @param sipiFileMetadata the metadata that Sipi returned about the file. */ -case class FileValueWithSipiMetadata(fileValue: FileValueV2, sipiFileMetadata: GetFileMetadataResponse) +case class FileValueWithSipiMetadata(fileValue: FileValueV2, sipiFileMetadata: FileMetadataSipiResponse) /** * Constructs [[FileValueWithSipiMetadata]] objects based on JSON-LD input. */ object FileValueWithSipiMetadata { def fromJsonLdObject( - jsonLDObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, FileValueWithSipiMetadata] = - ZIO.serviceWithZIO[StringFormatter] { stringFormatter => - for { - // The submitted value provides only Sipi's internal filename for the file. - internalFilename <- ZIO.attempt { - val validationFun: (String, => Nothing) => String = - (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) - jsonLDObject.requireStringWithValidation(FileValueHasFilename, validationFun) - } - - // Ask Sipi about the rest of the file's metadata. - tempFilePath <- ZIO.attempt(stringFormatter.makeSipiTempFilePath(internalFilename)) - fileMetadataResponse <- - MessageRelay.ask[GetFileMetadataResponse](GetFileMetadataRequest(tempFilePath, requestingUser)) - fileValue = FileValueV2( - internalFilename = internalFilename, - internalMimeType = fileMetadataResponse.internalMimeType, - originalFilename = fileMetadataResponse.originalFilename, - originalMimeType = fileMetadataResponse.originalMimeType - ) - } yield FileValueWithSipiMetadata(fileValue, fileMetadataResponse) - } + jsonLDObject: JsonLDObject + ): ZIO[SipiService, Throwable, FileValueWithSipiMetadata] = + for { + // The submitted value provides only Sipi's internal filename for the file. + internalFilename <- ZIO.attempt { + val validationFun: (String, => Nothing) => String = + (s, errorFun) => Iri.toSparqlEncodedString(s).getOrElse(errorFun) + jsonLDObject.requireStringWithValidation(FileValueHasFilename, validationFun) + } + meta <- SipiService.getFileMetadata(s"/tmp/$internalFilename") + fileValue = FileValueV2(internalFilename, meta.internalMimeType, meta.originalFilename, meta.originalMimeType) + } yield FileValueWithSipiMetadata(fileValue, meta) } /** @@ -2714,11 +2702,10 @@ case class StillImageFileValueContentV2( */ object StillImageFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, StillImageFileValueContentV2] = + jsonLDObject: JsonLDObject + ): ZIO[SipiService & StringFormatter, Throwable, StillImageFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject, requestingUser) + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) comment <- JsonLDUtil.getComment(jsonLDObject) } yield StillImageFileValueContentV2( ontologySchema = ApiV2Complex, @@ -2863,16 +2850,15 @@ case class ArchiveFileValueContentV2( */ object DocumentFileValueContentV2 { def fromJsonLdObject( - jsonLdObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, DocumentFileValueContentV2] = + jsonLdObject: JsonLDObject + ): ZIO[SipiService & StringFormatter, Throwable, DocumentFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLdObject, requestingUser) + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLdObject) comment <- JsonLDUtil.getComment(jsonLdObject) } yield DocumentFileValueContentV2( ontologySchema = ApiV2Complex, fileValue = fileValueWithSipiMetadata.fileValue, - pageCount = fileValueWithSipiMetadata.sipiFileMetadata.pageCount, + pageCount = fileValueWithSipiMetadata.sipiFileMetadata.numpages, dimX = fileValueWithSipiMetadata.sipiFileMetadata.width, dimY = fileValueWithSipiMetadata.sipiFileMetadata.height, comment @@ -2884,11 +2870,10 @@ object DocumentFileValueContentV2 { */ object ArchiveFileValueContentV2 { def fromJsonLdObject( - jsonLdObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, ArchiveFileValueContentV2] = + jsonLdObject: JsonLDObject + ): ZIO[SipiService & StringFormatter, Throwable, ArchiveFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLdObject, requestingUser) + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLdObject) comment <- JsonLDUtil.getComment(jsonLdObject) } yield ArchiveFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) } @@ -2957,13 +2942,11 @@ case class TextFileValueContentV2( */ object TextFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, TextFileValueContentV2] = - for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject, requestingUser) - comment <- JsonLDUtil.getComment(jsonLDObject) - } yield TextFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) + jsonLDObject: JsonLDObject + ): ZIO[SipiService & StringFormatter, Throwable, TextFileValueContentV2] = for { + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) + comment <- JsonLDUtil.getComment(jsonLDObject) + } yield TextFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) } /** @@ -3030,11 +3013,10 @@ case class AudioFileValueContentV2( */ object AudioFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, AudioFileValueContentV2] = + jsonLDObject: JsonLDObject + ): ZIO[SipiService & StringFormatter, Throwable, AudioFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject, requestingUser) + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) comment <- JsonLDUtil.getComment(jsonLDObject) } yield AudioFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) } @@ -3105,11 +3087,10 @@ case class MovingImageFileValueContentV2( */ object MovingImageFileValueContentV2 { def fromJsonLdObject( - jsonLDObject: JsonLDObject, - requestingUser: UserADM - ): ZIO[StringFormatter & MessageRelay, Throwable, MovingImageFileValueContentV2] = + jsonLDObject: JsonLDObject + ): ZIO[SipiService & StringFormatter, Throwable, MovingImageFileValueContentV2] = for { - fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject, requestingUser) + fileValueWithSipiMetadata <- FileValueWithSipiMetadata.fromJsonLdObject(jsonLDObject) comment <- JsonLDUtil.getComment(jsonLDObject) } yield MovingImageFileValueContentV2(ApiV2Complex, fileValueWithSipiMetadata.fileValue, comment) } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/IriService.scala b/webapi/src/main/scala/org/knora/webapi/responders/IriService.scala index 31652b7eff..5e508f0a03 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/IriService.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/IriService.scala @@ -78,7 +78,7 @@ final case class IriService( // Check that given entityIRI ends with a UUID ending: String = UuidUtil.fromIri(entityIriAsString) _ <- ZIO - .fromOption(UuidUtil.validateBase64EncodedUuid(ending)) + .fromTry(UuidUtil.base64Decode(ending)) .orElseFail(BadRequestException(s"IRI: '$entityIriAsString' must end with a valid base 64 UUID.")) } yield entityIriAsString diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala index ed12ebb2f6..0acd13eb93 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ListsResponderADM.scala @@ -39,6 +39,7 @@ import org.knora.webapi.messages.twirl.queries.sparql import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService import org.knora.webapi.responders.Responder +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.service.ProjectADMService import org.knora.webapi.store.triplestore.api.TriplestoreService import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Ask @@ -1056,7 +1057,9 @@ final case class ListsResponderADMLive( for { projectIri <- getProjectIriFromNode(nodeIri) // check if the requesting user is allowed to perform operation - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + _ = if ( + !requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { // not project or a system admin throw ForbiddenException(ListErrorMessages.ListChangePermission) } @@ -1065,7 +1068,7 @@ final case class ListsResponderADMLive( getUpdateNodeInfoSparqlStatement( changeNodeInfoRequest = ListNodeChangePayloadADM( listIri = ListIri.make(nodeIri).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(projectIri).fold(e => throw e.head, v => v), + projectIri = projectIri, name = Some(changeNodeNameRequest.name) ) ) @@ -1122,14 +1125,16 @@ final case class ListsResponderADMLive( projectIri <- getProjectIriFromNode(nodeIri) // check if the requesting user is allowed to perform operation - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + _ = if ( + !requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { // not project or a system admin throw ForbiddenException(ListErrorMessages.ListChangePermission) } changeNodeLabelsSparql <- getUpdateNodeInfoSparqlStatement( changeNodeInfoRequest = ListNodeChangePayloadADM( listIri = ListIri.make(nodeIri).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(projectIri).fold(e => throw e.head, v => v), + projectIri = projectIri, labels = Some(changeNodeLabelsRequest.labels) ) ) @@ -1185,7 +1190,9 @@ final case class ListsResponderADMLive( projectIri <- getProjectIriFromNode(nodeIri) // check if the requesting user is allowed to perform operation - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + _ = if ( + !requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { // not project or a system admin throw ForbiddenException(ListErrorMessages.ListChangePermission) } @@ -1193,7 +1200,7 @@ final case class ListsResponderADMLive( changeNodeCommentsSparql <- getUpdateNodeInfoSparqlStatement( ListNodeChangePayloadADM( listIri = ListIri.make(nodeIri).fold(e => throw e.head, v => v), - projectIri = ProjectIri.make(projectIri).fold(e => throw e.head, v => v), + projectIri = projectIri, comments = Some(changeNodeCommentsRequest.comments) ) ) @@ -1474,7 +1481,9 @@ final case class ListsResponderADMLive( dataNamedGraph <- getDataNamedGraph(projectIri) // check if the requesting user is allowed to perform operation - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + _ = if ( + !requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { // not project or a system admin throw ForbiddenException(ListErrorMessages.ListChangePermission) } @@ -1616,7 +1625,7 @@ final case class ListsResponderADMLive( */ def deleteListItem( nodeIri: IRI, - projectIri: IRI, + projectIri: ProjectIri, children: Seq[ListChildNodeADM], isRootNode: Boolean ): Task[IRI] = @@ -1714,7 +1723,9 @@ final case class ListsResponderADMLive( projectIri <- getProjectIriFromNode(nodeIri) // check if the requesting user is allowed to perform operation - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + _ = if ( + !requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin + ) { // not project or a system admin throw ForbiddenException(ListErrorMessages.ListChangePermission) } @@ -1822,7 +1833,7 @@ final case class ListsResponderADMLive( ): Task[String] = for { // get the data graph of the project. - dataNamedGraph <- getDataNamedGraph(changeNodeInfoRequest.projectIri.value) + dataNamedGraph <- getDataNamedGraph(changeNodeInfoRequest.projectIri) /* verify that the list name is unique for the project */ nodeNameUnique <- listNodeNameIsProjectUnique( @@ -1862,29 +1873,31 @@ final case class ListsResponderADMLive( * Helper method to get projectIri of a node. * * @param nodeIri the IRI of the node. - * @return a [[IRI]]. + * @return a [[ProjectIri]]. */ - private def getProjectIriFromNode(nodeIri: IRI): Task[IRI] = + private def getProjectIriFromNode(nodeIri: IRI): Task[ProjectIri] = for { maybeNode <- listNodeGetADM(nodeIri = nodeIri, shallow = true) - projectIri <- maybeNode match { - case Some(rootNode: ListRootNodeADM) => ZIO.attempt(rootNode.projectIri) - - case Some(childNode: ListChildNodeADM) => - for { - maybeRoot <- listNodeGetADM(nodeIri = childNode.hasRootNode, shallow = true) - rootProjectIri = maybeRoot match { - case Some(rootNode: ListRootNodeADM) => rootNode.projectIri - case _ => - throw BadRequestException( - s"Root node of $nodeIri was not found. Please verify the given IRI." - ) - } - } yield rootProjectIri - - case _ => throw BadRequestException(s"Node $nodeIri was not found. Please verify the given IRI.") - } + projectIriStr <- maybeNode match { + case Some(rootNode: ListRootNodeADM) => ZIO.succeed(rootNode.projectIri) + + case Some(childNode: ListChildNodeADM) => + for { + maybeRoot <- listNodeGetADM(childNode.hasRootNode, shallow = true) + iriStr <- maybeRoot.collect { case it: ListRootNodeADM => it } + .map(rootNode => ZIO.succeed(rootNode.projectIri)) + .getOrElse(ZIO.fail { + val msg = + s"Root node of $nodeIri was not found. Please verify the given IRI." + BadRequestException(msg) + }) + } yield iriStr + + case _ => + throw BadRequestException(s"Node $nodeIri was not found. Please verify the given IRI.") + } + projectIri <- ProjectIri.from(projectIriStr).toZIO.mapError(e => BadRequestException(e.getMessage)) } yield projectIri /** @@ -1906,11 +1919,10 @@ final case class ListsResponderADMLive( * @param projectIri the IRI of the project. * @return an [[IRI]]. */ - private def getDataNamedGraph(projectIri: IRI): Task[IRI] = + private def getDataNamedGraph(projectIri: ProjectIri): Task[IRI] = for { - projectId <- IriIdentifier.fromString(projectIri).toZIO.mapError(e => BadRequestException(e.getMessage)) project <- messageRelay - .ask[Option[ProjectADM]](ProjectGetADM(projectId)) + .ask[Option[ProjectADM]](ProjectGetADM(IriIdentifier.from(projectIri))) .someOrFail(BadRequestException(s"Project '$projectIri' not found.")) } yield ProjectADMService.projectDataNamedGraphV2(project).value diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index 62b754411b..d1b13f5c95 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -40,6 +40,7 @@ import org.knora.webapi.responders.Responder import org.knora.webapi.slice.admin.AdminConstants import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.service.ProjectADMService import org.knora.webapi.store.cache.settings.CacheServiceSettings import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -114,7 +115,7 @@ trait ProjectsResponderADM { * @param projectIri the IRI of the project. * @return keywords for a projects as [[ProjectKeywordsGetResponseADM]] */ - def projectKeywordsGetRequestADM(projectIri: Iri.ProjectIri): Task[ProjectKeywordsGetResponseADM] + def projectKeywordsGetRequestADM(projectIri: ProjectIri): Task[ProjectKeywordsGetResponseADM] /** * Get project's restricted view settings. @@ -166,7 +167,7 @@ trait ProjectsResponderADM { * [[ForbiddenException]] In the case that the user is not allowed to perform the operation. */ def changeBasicInformationRequestADM( - projectIri: Iri.ProjectIri, + projectIri: ProjectIri, updateReq: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID @@ -389,7 +390,7 @@ final case class ProjectsResponderADMLive( * @param projectIri the IRI of the project. * @return keywords for a projects as [[ProjectKeywordsGetResponseADM]] */ - override def projectKeywordsGetRequestADM(projectIri: Iri.ProjectIri): Task[ProjectKeywordsGetResponseADM] = + override def projectKeywordsGetRequestADM(projectIri: ProjectIri): Task[ProjectKeywordsGetResponseADM] = for { id <- IriIdentifier.fromString(projectIri.value).toZIO.mapError(e => BadRequestException(e.getMessage)) keywords <- projectService @@ -466,7 +467,7 @@ final case class ProjectsResponderADMLive( * [[ForbiddenException]] In the case that the user is not allowed to perform the operation. */ override def changeBasicInformationRequestADM( - projectIri: Iri.ProjectIri, + projectIri: ProjectIri, updateReq: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID @@ -476,7 +477,7 @@ final case class ProjectsResponderADMLive( * The actual change project task run with an IRI lock. */ def changeProjectTask( - projectIri: Iri.ProjectIri, + projectIri: ProjectIri, updateReq: ProjectUpdateRequest, requestingUser: UserADM ): Task[ProjectOperationResponseADM] = @@ -503,7 +504,7 @@ final case class ProjectsResponderADMLive( * * [[NotFoundException]] In the case that the project's IRI is not found. */ - private def updateProjectADM(projectIri: Iri.ProjectIri, projectUpdatePayload: ProjectUpdateRequest) = { + private def updateProjectADM(projectIri: ProjectIri, projectUpdatePayload: ProjectUpdateRequest) = { val areAllParamsNone: Boolean = projectUpdatePayload.productIterator.forall { case param: Option[Any] => param.isEmpty diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala index 86ba2dbff4..5c743aa465 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/SipiResponderADM.scala @@ -8,7 +8,6 @@ package org.knora.webapi.responders.admin import com.typesafe.scalalogging.LazyLogging import zio.* -import dsp.errors.BadRequestException import dsp.errors.InconsistentRepositoryDataException import dsp.errors.NotFoundException import org.knora.webapi.core.MessageHandler @@ -16,7 +15,7 @@ import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.ResponderRequest import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter -import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* +import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestrictedViewSettingsADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectRestrictedViewSettingsGetADM import org.knora.webapi.messages.admin.responder.sipimessages.SipiFileInfoGetRequestADM @@ -118,14 +117,11 @@ final case class SipiResponderADMLive( response <- permissionCode match { case 1 => for { - maybeRVSettings <- messageRelay - .ask[Option[ProjectRestrictedViewSettingsADM]]( - ProjectRestrictedViewSettingsGetADM( - identifier = ShortcodeIdentifier - .fromString(request.projectID) - .getOrElseWith(e => throw BadRequestException(e.head.getMessage)) - ) - ) + maybeRVSettings <- + messageRelay + .ask[Option[ProjectRestrictedViewSettingsADM]]( + ProjectRestrictedViewSettingsGetADM(ShortcodeIdentifier.from(request.projectID)) + ) } yield SipiFileInfoGetResponseADM(permissionCode = permissionCode, maybeRVSettings) case _ => diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala index 935eff852c..e26d1878ab 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ResourcesResponderV2.scala @@ -15,6 +15,7 @@ import scala.concurrent.Future import dsp.errors.* import dsp.valueobjects.Iri import dsp.valueobjects.UuidUtil +import org.knora.webapi.SchemaAndOptions.apiV2SchemaWithOption import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageHandler @@ -44,7 +45,6 @@ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.OwlCardinality.* import org.knora.webapi.messages.v2.responder.ontologymessages.* import org.knora.webapi.messages.v2.responder.resourcemessages.* -import org.knora.webapi.messages.v2.responder.searchmessages.GravsearchRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingRequestV2 import org.knora.webapi.messages.v2.responder.standoffmessages.GetMappingResponseV2 import org.knora.webapi.messages.v2.responder.standoffmessages.GetXSLTransformationRequestV2 @@ -78,6 +78,7 @@ final case class ResourcesResponderV2Live( resourceUtilV2: ResourceUtilV2, permissionUtilADM: PermissionUtilADM, projectRepo: KnoraProjectRepo, + searchResponderV2: SearchResponderV2, implicit val stringFormatter: StringFormatter ) extends ResourcesResponderV2 with MessageHandler @@ -1757,9 +1758,9 @@ final case class ResourcesResponderV2Live( .map(_.replace("$resourceIri", resourceIri)) .mapAttempt(GravsearchParser.parseQuery) - // do a request to the SearchResponder - req = GravsearchRequestV2(query, ApiV2Complex, SchemaOptions.ForStandoffWithTextValues, requestingUser) - resource <- messageRelay.ask[ReadResourcesSequenceV2](req).mapAttempt(_.toResource(resourceIri)) + resource <- searchResponderV2 + .gravsearchV2(query, apiV2SchemaWithOption(MarkupAsXml), requestingUser) + .mapAttempt(_.toResource(resourceIri)) } yield resource } else { @@ -2269,15 +2270,11 @@ final case class ResourcesResponderV2Live( // Run the query. parsedGravsearchQuery <- ZIO.succeed(GravsearchParser.parseQuery(gravsearchQueryForIncomingLinks)) - searchResponse <- messageRelay - .ask[ReadResourcesSequenceV2]( - GravsearchRequestV2( - constructQuery = parsedGravsearchQuery, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffSeparateFromTextValues, - requestingUser = request.requestingUser - ) - ) + searchResponse <- searchResponderV2.gravsearchV2( + parsedGravsearchQuery, + apiV2SchemaWithOption(MarkupAsStandoff), + request.requestingUser + ) resource = searchResponse.toResource(request.resourceIri) incomingLinks = resource.values.getOrElse(OntologyConstants.KnoraBase.HasIncomingLinkValue.toSmartIri, Seq.empty) @@ -2861,7 +2858,7 @@ final case class ResourcesResponderV2Live( object ResourcesResponderV2Live { val layer: URLayer[ - AppConfig & ConstructResponseUtilV2 & IriService & KnoraProjectRepo & MessageRelay & PermissionUtilADM & ResourceUtilV2 & StandoffTagUtilV2 & StringFormatter & TriplestoreService, + AppConfig & ConstructResponseUtilV2 & IriService & KnoraProjectRepo & MessageRelay & PermissionUtilADM & ResourceUtilV2 & StandoffTagUtilV2 & SearchResponderV2 & StringFormatter & TriplestoreService, ResourcesResponderV2 ] = ZLayer.fromZIO { for { @@ -2874,8 +2871,9 @@ object ResourcesResponderV2Live { ru <- ZIO.service[ResourceUtilV2] pu <- ZIO.service[PermissionUtilADM] pr <- ZIO.service[KnoraProjectRepo] + sr <- ZIO.service[SearchResponderV2] sf <- ZIO.service[StringFormatter] - handler <- mr.subscribe(ResourcesResponderV2Live(config, iriS, mr, ts, cu, su, ru, pu, pr, sf)) + handler <- mr.subscribe(ResourcesResponderV2Live(config, iriS, mr, ts, cu, su, ru, pu, pr, sr, sf)) } yield handler } } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchQueries.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchQueries.scala new file mode 100644 index 0000000000..b4b6c21b8f --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchQueries.scala @@ -0,0 +1,101 @@ +/* + * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.responders.v2 + +import org.knora.webapi.IRI +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Construct +import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select + +object SearchQueries { + + def selectCountByLabel( + searchTerm: String, + limitToProject: Option[IRI], + limitToResourceClass: Option[IRI] + ): Select = + Select( + s"""|PREFIX rdfs: + |PREFIX knora-base: + |SELECT (count(distinct ?resource) as ?count) + |WHERE { + | ?resource (rdfs:label "$searchTerm") ; + | a ?resourceClass . + | ?resourceClass rdfs:subClassOf* knora-base:Resource . + | ${limitToResourceClass.fold("")(resourceClass => s"?resourceClass rdfs:subClassOf* <$resourceClass> .")} + | ${limitToProject.fold("")(project => s"?resource knora-base:attachedToProject <$project> .")} + | FILTER NOT EXISTS { ?resource knora-base:isDeleted true . } + |} + |""".stripMargin + ) + + def constructSearchByLabel( + searchTerm: String, + limitToResourceClass: Option[IRI] = None, + limitToProject: Option[IRI] = None, + limit: Int, + offset: Int = 0 + ): Construct = { + val limitToClassOrProject = + (limitToResourceClass, limitToProject) match { + case (Some(cls), _) => s"?resourceClass rdfs:subClassOf* <$cls> ." + case (_, Some(project)) => s"?resource knora-base:attachedToProject <$project> ." + case _ => "" + } + Construct( + s"""|PREFIX rdfs: + |PREFIX knora-base: + |CONSTRUCT { + | ?resource rdfs:label ?label ; + | a knora-base:Resource ; + | knora-base:isMainResource true ; + | knora-base:isDeleted false ; + | a ?resourceType ; + | knora-base:attachedToUser ?resourceCreator ; + | knora-base:hasPermissions ?resourcePermissions ; + | knora-base:attachedToProject ?resourceProject ; + | knora-base:creationDate ?creationDate ; + | knora-base:lastModificationDate ?lastModificationDate ; + | knora-base:hasValue ?valueObject ; + | ?resourceValueProperty ?valueObject . + | ?valueObject ?valueObjectProperty ?valueObjectValue . + |} WHERE { + | { + | SELECT DISTINCT ?resource ?label + | WHERE { + | ?resource (rdfs:label "$searchTerm") ; + | a ?resourceClass ; + | rdfs:label ?label . + | $limitToClassOrProject + | FILTER NOT EXISTS { ?resource knora-base:isDeleted true . } + | } + | ORDER BY ?resource + | LIMIT $limit + | OFFSET $offset + | } + | + | ?resource a ?resourceType ; + | knora-base:attachedToUser ?resourceCreator ; + | knora-base:hasPermissions ?resourcePermissions ; + | knora-base:attachedToProject ?resourceProject ; + | knora-base:creationDate ?creationDate ; + | rdfs:label ?label . + | OPTIONAL { ?resource knora-base:lastModificationDate ?lastModificationDate . } + | OPTIONAL { + | ?resource ?resourceValueProperty ?valueObject . + | ?resourceValueProperty rdfs:subPropertyOf* knora-base:hasValue . + | ?valueObject a ?valueObjectType ; + | ?valueObjectProperty ?valueObjectValue . + | ?valueObjectType rdfs:subClassOf* knora-base:Value . + | FILTER(?valueObjectType != knora-base:LinkValue) + | FILTER NOT EXISTS { ?valueObject knora-base:isDeleted true . } + | FILTER NOT EXISTS { ?valueObjectValue a knora-base:StandoffTag . } + | } + |} + |""".stripMargin + ) + } + +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala index 6eb2c23355..d55493993b 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/SearchResponderV2.scala @@ -7,6 +7,7 @@ package org.knora.webapi.responders.v2 import com.typesafe.scalalogging.LazyLogging import zio.* +import zio.macros.accessible import dsp.errors.AssertionException import dsp.errors.BadRequestException @@ -14,11 +15,9 @@ import dsp.errors.GravsearchException import dsp.errors.InconsistentRepositoryDataException import org.knora.webapi.* import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.MessageHandler import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.IriConversions.* import org.knora.webapi.messages.OntologyConstants -import org.knora.webapi.messages.ResponderRequest import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.usersmessages.UserADM @@ -27,13 +26,16 @@ import org.knora.webapi.messages.twirl.queries.sparql import org.knora.webapi.messages.util.ConstructResponseUtilV2 import org.knora.webapi.messages.util.ConstructResponseUtilV2.MappingAndXSLTransformation import org.knora.webapi.messages.util.ErrorHandlingMap +import org.knora.webapi.messages.util.rdf.JsonLDDocument +import org.knora.webapi.messages.util.rdf.JsonLDInt +import org.knora.webapi.messages.util.rdf.JsonLDObject +import org.knora.webapi.messages.util.rdf.JsonLDString import org.knora.webapi.messages.util.rdf.SparqlSelectResult import org.knora.webapi.messages.util.rdf.SparqlSelectResultBody import org.knora.webapi.messages.util.rdf.VariableResultsRow import org.knora.webapi.messages.util.search.* import org.knora.webapi.messages.util.search.gravsearch.GravsearchQueryChecker import org.knora.webapi.messages.util.search.gravsearch.mainquery.GravsearchMainQueryGenerator -import org.knora.webapi.messages.util.search.gravsearch.prequery.AbstractPrequeryGenerator import org.knora.webapi.messages.util.search.gravsearch.prequery.GravsearchToCountPrequeryTransformer import org.knora.webapi.messages.util.search.gravsearch.prequery.GravsearchToPrequeryTransformer import org.knora.webapi.messages.util.search.gravsearch.prequery.InferenceOptimizationService @@ -48,8 +50,6 @@ import org.knora.webapi.messages.v2.responder.ontologymessages.EntityInfoGetResp import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadPropertyInfoV2 import org.knora.webapi.messages.v2.responder.resourcemessages.* -import org.knora.webapi.messages.v2.responder.searchmessages.* -import org.knora.webapi.responders.Responder import org.knora.webapi.slice.ontology.repo.service.OntologyCache import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -57,7 +57,151 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Constru import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Select import org.knora.webapi.util.ApacheLuceneSupport.* -trait SearchResponderV2 +/** + * Represents the number of resources found by a search query. + */ +case class ResourceCountV2(numberOfResources: Int) extends KnoraJsonLDResponseV2 { + override def toJsonLDDocument( + targetSchema: ApiV2Schema, + appConfig: AppConfig, + schemaOptions: Set[SchemaOption] + ): JsonLDDocument = + JsonLDDocument( + body = JsonLDObject( + Map( + OntologyConstants.SchemaOrg.NumberOfItems -> JsonLDInt(numberOfResources) + ) + ), + context = JsonLDObject( + Map( + "schema" -> JsonLDString(OntologyConstants.SchemaOrg.SchemaOrgPrefixExpansion) + ) + ) + ) +} +@accessible +trait SearchResponderV2 { + + /** + * Performs a search using a Gravsearch query provided by the client. + * + * @param query a Gravsearch query provided by the client. + * @param schemaAndOptions the target API schema and its options submitted with the request. + * @param user the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ + def gravsearchV2( + query: ConstructQuery, + schemaAndOptions: SchemaAndOptions[ApiV2Schema, SchemaOption], + user: UserADM + ): Task[ReadResourcesSequenceV2] + + /** + * Performs a count query for a Gravsearch query provided by the user. + * + * @param query a Gravsearch query provided by the client. + * @param user the client making the request. + * @return a [[ResourceCountV2]] representing the number of resources that have been found. + */ + def gravsearchCountV2(query: ConstructQuery, user: UserADM): Task[ResourceCountV2] + + /** + * Performs a fulltext search and returns the resources count (how many resources match the search criteria), + * without taking into consideration permission checking. + * + * This method does not return the resources themselves. + * + * @param searchValue the values to search for. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @return a [[ResourceCountV2]] representing the number of resources that have been found. + */ + def fulltextSearchCountV2( + searchValue: IRI, + limitToProject: Option[IRI], + limitToResourceClass: Option[SmartIri], + limitToStandoffClass: Option[SmartIri] + ): Task[ResourceCountV2] + + /** + * Performs a fulltext search (simple search). + * + * @param searchValue the values to search for. + * @param offset the offset to be used for paging. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @param limitToStandoffClass limit search to given standoff class. + * @param returnFiles if true, return any file value attached to each matching resource. + * @param schemaAndOptions the target API schema and the schema options submitted with the request. + * @param requestingUser the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ + def fulltextSearchV2( + searchValue: IRI, + offset: RuntimeFlags, + limitToProject: Option[IRI], + limitToResourceClass: Option[SmartIri], + limitToStandoffClass: Option[SmartIri], + returnFiles: Boolean, + schemaAndOptions: SchemaAndOptions[ApiV2Schema, SchemaOption], + requestingUser: UserADM + ): Task[ReadResourcesSequenceV2] + + /** + * Performs a count query for a search for resources by their rdfs:label. + * + * @param searchValue the values to search for. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @return a [[ResourceCountV2]] representing the resources that have been found. + */ + def searchResourcesByLabelCountV2( + searchValue: IRI, + limitToProject: Option[IRI], + limitToResourceClass: Option[SmartIri] + ): Task[ResourceCountV2] + + /** + * Performs a search for resources by their rdfs:label. + * + * @param searchValue the values to search for. + * @param offset the offset to be used for paging. + * @param limitToProject limit search to given project. + * @param limitToResourceClass limit search to given resource class. + * @param targetSchema the schema of the response. + * @param requestingUser the client making the request. + * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. + */ + def searchResourcesByLabelV2( + searchValue: IRI, + offset: RuntimeFlags, + limitToProject: Option[IRI], + limitToResourceClass: Option[SmartIri], + targetSchema: ApiV2Schema, + requestingUser: UserADM + ): Task[ReadResourcesSequenceV2] + + /** + * Requests resources of the specified class from the specified project. + * + * @param projectIri the IRI of the project. + * @param resourceClass the IRI of the resource class, in the complex schema. + * @param orderByProperty the IRI of the property that the resources are to be ordered by, in the complex schema. + * @param page the page number of the results page to be returned. + * @param schemaAndOptions the schema of the response and schema options submitted with the request. + * @param requestingUser the user making the request. + * @return a [[ReadResourcesSequenceV2]]. + */ + def searchResourcesByProjectAndClassV2( + projectIri: SmartIri, + resourceClass: SmartIri, + orderByProperty: Option[SmartIri], + page: RuntimeFlags, + schemaAndOptions: SchemaAndOptions[ApiV2Schema, SchemaOption], + requestingUser: UserADM + ): Task[ReadResourcesSequenceV2] +} + final case class SearchResponderV2Live( private val appConfig: AppConfig, private val triplestore: TriplestoreService, @@ -73,90 +217,8 @@ final case class SearchResponderV2Live( private val iriConverter: IriConverter, private val constructTransformer: ConstructTransformer ) extends SearchResponderV2 - with MessageHandler with LazyLogging { - override def isResponsibleFor(message: ResponderRequest): Boolean = - message.isInstanceOf[SearchResponderRequestV2] - override def handle(msg: ResponderRequest): Task[KnoraJsonLDResponseV2] = msg match { - case FullTextSearchCountRequestV2( - searchValue, - limitToProject, - limitToResourceClass, - limitToStandoffClass, - _ - ) => - fulltextSearchCountV2(searchValue, limitToProject, limitToResourceClass, limitToStandoffClass) - - case FulltextSearchRequestV2( - searchValue, - offset, - limitToProject, - limitToResourceClass, - limitToStandoffClass, - returnFiles, - targetSchema, - schemaOptions, - requestingUser - ) => - fulltextSearchV2( - searchValue, - offset, - limitToProject, - limitToResourceClass, - limitToStandoffClass, - returnFiles, - targetSchema, - schemaOptions, - requestingUser, - appConfig - ) - - case GravsearchCountRequestV2(query, requestingUser) => - gravsearchCountV2( - inputQuery = query, - requestingUser = requestingUser - ) - - case GravsearchRequestV2(query, targetSchema, schemaOptions, requestingUser) => - gravsearchV2( - inputQuery = query, - targetSchema = targetSchema, - schemaOptions = schemaOptions, - requestingUser = requestingUser - ) - - case SearchResourceByLabelCountRequestV2( - searchValue, - limitToProject, - limitToResourceClass, - _ - ) => - searchResourcesByLabelCountV2(searchValue, limitToProject, limitToResourceClass) - - case SearchResourceByLabelRequestV2( - searchValue, - offset, - limitToProject, - limitToResourceClass, - targetSchema, - requestingUser - ) => - searchResourcesByLabelV2( - searchValue, - offset, - limitToProject, - limitToResourceClass, - targetSchema, - requestingUser - ) - - case resourcesInProjectGetRequestV2: SearchResourcesByProjectAndClassRequestV2 => - searchResourcesByProjectAndClassV2(resourcesInProjectGetRequestV2) - - case other => Responder.handleUnexpectedMessage(other, this.getClass.getName) - } - /** * Performs a fulltext search and returns the resources count (how many resources match the search criteria), * without taking into consideration permission checking. @@ -169,12 +231,12 @@ final case class SearchResponderV2Live( * * @return a [[ResourceCountV2]] representing the number of resources that have been found. */ - private def fulltextSearchCountV2( + override def fulltextSearchCountV2( searchValue: IRI, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri], limitToStandoffClass: Option[SmartIri] - ) = + ): Task[ResourceCountV2] = for { countSparql <- ZIO.attempt( sparql.v2.txt @@ -209,41 +271,32 @@ final case class SearchResponderV2Live( * @param limitToResourceClass limit search to given resource class. * @param limitToStandoffClass limit search to given standoff class. * @param returnFiles if true, return any file value attached to each matching resource. - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. + * @param schemaAndOptions the target API schema and the schema options submitted with the request. * @param requestingUser the client making the request. - * @param appConfig the application config * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. */ - private def fulltextSearchV2( + override def fulltextSearchV2( searchValue: String, offset: Int, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri], limitToStandoffClass: Option[SmartIri], returnFiles: Boolean, - targetSchema: ApiV2Schema, - schemaOptions: Set[SchemaOption], - requestingUser: UserADM, - appConfig: AppConfig + schemaAndOptions: SchemaAndOptions[ApiV2Schema, SchemaOption], + requestingUser: UserADM ): Task[ReadResourcesSequenceV2] = { import org.knora.webapi.messages.util.search.FullTextMainQueryGenerator.FullTextSearchConstants - - val groupConcatSeparator = StringFormatter.INFORMATION_SEPARATOR_ONE - - val searchTerms: LuceneQueryString = LuceneQueryString(searchValue) - for { searchSparql <- ZIO.attempt( sparql.v2.txt .searchFulltext( - searchTerms = searchTerms, + searchTerms = LuceneQueryString(searchValue), limitToProject = limitToProject, limitToResourceClass = limitToResourceClass.map(_.toString), limitToStandoffClass = limitToStandoffClass.map(_.toString), returnFiles = returnFiles, - separator = Some(groupConcatSeparator), + separator = Some(StringFormatter.INFORMATION_SEPARATOR_ONE), limit = appConfig.v2.resourcesSequence.resultsPerPage, offset = offset * appConfig.v2.resourcesSequence.resultsPerPage, // determine the actual offset countQuery = false @@ -277,7 +330,10 @@ final case class SearchResponderV2Live( case Some(valObjIris) => // Filter out empty IRIs (which we could get if a variable used in GROUP_CONCAT is unbound) - acc + (mainResIri -> valObjIris.split(groupConcatSeparator).toSet.filterNot(_.isEmpty)) + acc + (mainResIri -> valObjIris + .split(StringFormatter.INFORMATION_SEPARATOR_ONE) + .toSet + .filterNot(_.isEmpty)) case None => acc } @@ -290,8 +346,8 @@ final case class SearchResponderV2Live( val mainQuery = FullTextMainQueryGenerator.createMainQuery( resourceIris = resourceIris.toSet, valueObjectIris = allValueObjectIris, - targetSchema = targetSchema, - schemaOptions = schemaOptions + targetSchema = schemaAndOptions.schema, + schemaOptions = schemaAndOptions.options ) for { @@ -308,8 +364,8 @@ final case class SearchResponderV2Live( // Find out whether to query standoff along with text values. This boolean value will be passed to // ConstructResponseUtilV2.makeTextValueContentV2. queryStandoff: Boolean = SchemaOptions.queryStandoffWithTextValues( - targetSchema = targetSchema, - schemaOptions = schemaOptions + targetSchema = schemaAndOptions.schema, + schemaOptions = schemaAndOptions.options ) // If we're querying standoff, get XML-to standoff mappings. @@ -332,52 +388,41 @@ final case class SearchResponderV2Live( queryStandoff = queryStandoff, calculateMayHaveMoreResults = true, versionDate = None, - targetSchema = targetSchema, + targetSchema = schemaAndOptions.schema, requestingUser = requestingUser ) } yield apiResponse } - /** - * Performs a count query for a Gravsearch query provided by the user. - * - * @param inputQuery a Gravsearch query provided by the client. - * - * @param requestingUser the client making the request. - * @return a [[ResourceCountV2]] representing the number of resources that have been found. - */ - private def gravsearchCountV2( - inputQuery: ConstructQuery, - requestingUser: UserADM - ): Task[ResourceCountV2] = + override def gravsearchCountV2(query: ConstructQuery, user: UserADM): Task[ResourceCountV2] = for { _ <- // make sure that OFFSET is 0 ZIO - .fail(GravsearchException(s"OFFSET must be 0 for a count query, but ${inputQuery.offset} given")) - .when(inputQuery.offset != 0) + .fail(GravsearchException(s"OFFSET must be 0 for a count query, but ${query.offset} given")) + .when(query.offset != 0) // Do type inspection and remove type annotations from the WHERE clause. - typeInspectionResult <- gravsearchTypeInspectionRunner.inspectTypes(inputQuery.whereClause, requestingUser) + typeInspectionResult <- gravsearchTypeInspectionRunner.inspectTypes(query.whereClause, user) - whereClauseWithoutAnnotations <- GravsearchTypeInspectionUtil.removeTypeAnnotations(inputQuery.whereClause) + whereClauseWithoutAnnotations <- GravsearchTypeInspectionUtil.removeTypeAnnotations(query.whereClause) // Validate schemas and predicates in the CONSTRUCT clause. - _ <- GravsearchQueryChecker.checkConstructClause(inputQuery.constructClause, typeInspectionResult) + _ <- GravsearchQueryChecker.checkConstructClause(query.constructClause, typeInspectionResult) // Create a Select prequery querySchema <- - ZIO.fromOption(inputQuery.querySchema).orElseFail(AssertionException(s"WhereClause has no querySchema")) + ZIO.fromOption(query.querySchema).orElseFail(AssertionException(s"WhereClause has no querySchema")) gravsearchToCountTransformer: GravsearchToCountPrequeryTransformer = new GravsearchToCountPrequeryTransformer( - constructClause = inputQuery.constructClause, + constructClause = query.constructClause, typeInspectionResult = typeInspectionResult, querySchema = querySchema ) prequery <- queryTraverser.transformConstructToSelect( - inputQuery = inputQuery.copy( + inputQuery = query.copy( whereClause = whereClauseWithoutAnnotations, orderBy = Seq.empty[OrderCriterion] // count queries do not need any sorting criteria ), @@ -392,7 +437,7 @@ final case class SearchResponderV2Live( ) ontologiesForInferenceMaybe <- - inferenceOptimizationService.getOntologiesRelevantForInference(inputQuery.whereClause) + inferenceOptimizationService.getOntologiesRelevantForInference(query.whereClause) countQuery <- queryTraverser.transformSelectToSelect( inputQuery = prequery, @@ -415,37 +460,26 @@ final case class SearchResponderV2Live( } yield ResourceCountV2(numberOfResources = count.toInt) - /** - * Performs a search using a Gravsearch query provided by the client. - * - * @param inputQuery a Gravsearch query provided by the client. - * @param targetSchema the target API schema. - * @param schemaOptions the schema options submitted with the request. - * - * @param requestingUser the client making the request. - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ - private def gravsearchV2( - inputQuery: ConstructQuery, - targetSchema: ApiV2Schema, - schemaOptions: Set[SchemaOption], - requestingUser: UserADM + override def gravsearchV2( + query: ConstructQuery, + schemaAndOptions: SchemaAndOptions[ApiV2Schema, SchemaOption], + user: UserADM ): Task[ReadResourcesSequenceV2] = { for { // Do type inspection and remove type annotations from the WHERE clause. - typeInspectionResult <- gravsearchTypeInspectionRunner.inspectTypes(inputQuery.whereClause, requestingUser) - whereClauseWithoutAnnotations <- GravsearchTypeInspectionUtil.removeTypeAnnotations(inputQuery.whereClause) + typeInspectionResult <- gravsearchTypeInspectionRunner.inspectTypes(query.whereClause, user) + whereClauseWithoutAnnotations <- GravsearchTypeInspectionUtil.removeTypeAnnotations(query.whereClause) // Validate schemas and predicates in the CONSTRUCT clause. - _ <- GravsearchQueryChecker.checkConstructClause(inputQuery.constructClause, typeInspectionResult) + _ <- GravsearchQueryChecker.checkConstructClause(query.constructClause, typeInspectionResult) // Create a Select prequery querySchema <- - ZIO.fromOption(inputQuery.querySchema).orElseFail(AssertionException(s"InputQuery has no querySchema")) + ZIO.fromOption(query.querySchema).orElseFail(AssertionException(s"InputQuery has no querySchema")) gravsearchToPrequeryTransformer: GravsearchToPrequeryTransformer = new GravsearchToPrequeryTransformer( - constructClause = inputQuery.constructClause, + constructClause = query.constructClause, typeInspectionResult = typeInspectionResult, querySchema = querySchema, appConfig = appConfig @@ -455,11 +489,11 @@ final case class SearchResponderV2Live( // TODO: the ORDER BY criterion has to be included in a GROUP BY statement, returning more than one row if property occurs more than once ontologiesForInferenceMaybe <- - inferenceOptimizationService.getOntologiesRelevantForInference(inputQuery.whereClause) + inferenceOptimizationService.getOntologiesRelevantForInference(query.whereClause) prequery <- queryTraverser.transformConstructToSelect( - inputQuery = inputQuery.copy(whereClause = whereClauseWithoutAnnotations), + inputQuery = query.copy(whereClause = whereClauseWithoutAnnotations), transformer = gravsearchToPrequeryTransformer ) @@ -484,17 +518,11 @@ final case class SearchResponderV2Live( prequerySparql = transformedPrequery.toSparql - start <- Clock.instant.map(_.toEpochMilli) prequeryResponseNotMerged <- triplestore .query(Select(prequerySparql, isGravsearch = true)) .logError(s"Gravsearch timed out for prequery:\n$prequerySparql") - end <- Clock.instant.map(_.toEpochMilli) - duration = (end - start) / 1000.0 - _ = if (duration < 3) logger.debug(s"Prequery took: ${duration}s") - else logger.warn(s"Slow Prequery ($duration):\n$prequerySparql") - pageSizeBeforeFiltering: Int = prequeryResponseNotMerged.results.bindings.size // Merge rows with the same main resource IRI. This could happen if there are unbound variables in a UNION. @@ -564,8 +592,8 @@ final case class SearchResponderV2Live( mainResourceIris = mainResourceIris.map(iri => IriRef(iri.toSmartIri)).toSet, dependentResourceIris = allDependentResourceIris.map(iri => IriRef(iri.toSmartIri)), valueObjectIris = allValueObjectIris, - targetSchema = targetSchema, - schemaOptions = schemaOptions + targetSchema = schemaAndOptions.schema, + schemaOptions = schemaAndOptions.options ) for { @@ -574,7 +602,7 @@ final case class SearchResponderV2Live( // Filter out values that the user doesn't have permission to see. queryResultsFilteredForPermissions = - constructResponseUtilV2.splitMainResourcesAndValueRdfData(mainQueryResponse, requestingUser) + constructResponseUtilV2.splitMainResourcesAndValueRdfData(mainQueryResponse, user) // filter out those value objects that the user does not want to be returned by the query (not present in the input query's CONSTRUCT clause) queryResWithFullGraphPatternOnlyRequestedValues: Map[ @@ -600,14 +628,14 @@ final case class SearchResponderV2Live( // Find out whether to query standoff along with text values. This boolean value will be passed to // ConstructResponseUtilV2.makeTextValueContentV2. queryStandoff: Boolean = SchemaOptions.queryStandoffWithTextValues( - targetSchema = targetSchema, - schemaOptions = schemaOptions + targetSchema = schemaAndOptions.schema, + schemaOptions = schemaAndOptions.options ) // If we're querying standoff, get XML-to standoff mappings. mappingsAsMap <- if (queryStandoff) { - constructResponseUtilV2.getMappingsFromQueryResultsSeparated(mainQueryResults.resources, requestingUser) + constructResponseUtilV2.getMappingsFromQueryResultsSeparated(mainQueryResults.resources, user) } else { ZIO.succeed(Map.empty[IRI, MappingAndXSLTransformation]) } @@ -620,24 +648,34 @@ final case class SearchResponderV2Live( queryStandoff = queryStandoff, versionDate = None, calculateMayHaveMoreResults = true, - targetSchema = targetSchema, - requestingUser = requestingUser + targetSchema = schemaAndOptions.schema, + requestingUser = user ) } yield apiResponse } /** - * Gets resources from a project. + * Requests resources of the specified class from the specified project. * - * @param resourcesInProjectGetRequestV2 the request message. + * @param projectIri the IRI of the project. + * @param resourceClass the IRI of the resource class, in the complex schema. + * @param orderByProperty the IRI of the property that the resources are to be ordered by, in the complex schema. + * @param page the page number of the results page to be returned. + * @param schemaAndOptions the schema of the response and schema options submitted with the request. + * @param requestingUser the user making the request. * @return a [[ReadResourcesSequenceV2]]. */ - private def searchResourcesByProjectAndClassV2( - resourcesInProjectGetRequestV2: SearchResourcesByProjectAndClassRequestV2 + override def searchResourcesByProjectAndClassV2( + projectIri: SmartIri, + resourceClass: SmartIri, + orderByProperty: Option[SmartIri], + page: Int, + schemaAndOptions: SchemaAndOptions[ApiV2Schema, SchemaOption], + requestingUser: UserADM ): Task[ReadResourcesSequenceV2] = { - val internalClassIri = resourcesInProjectGetRequestV2.resourceClass.toOntologySchema(InternalSchema) + val internalClassIri = resourceClass.toOntologySchema(InternalSchema) val maybeInternalOrderByPropertyIri: Option[SmartIri] = - resourcesInProjectGetRequestV2.orderByProperty.map(_.toOntologySchema(InternalSchema)) + orderByProperty.map(_.toOntologySchema(InternalSchema)) for { // Get information about the resource class, and about the ORDER BY property if specified. @@ -645,7 +683,7 @@ final case class SearchResponderV2Live( EntityInfoGetRequestV2( classIris = Set(internalClassIri), propertyIris = maybeInternalOrderByPropertyIri.toSet, - requestingUser = resourcesInProjectGetRequestV2.requestingUser + requestingUser = requestingUser ) ) @@ -666,7 +704,7 @@ final case class SearchResponderV2Live( !internalOrderByPropertyDef.isResourceProp || internalOrderByPropertyDef.isLinkProp || internalOrderByPropertyDef.isLinkValueProp || internalOrderByPropertyDef.isFileValueProp ) { throw BadRequestException( - s"Cannot sort by property <${resourcesInProjectGetRequestV2.orderByProperty}>" + s"Cannot sort by property <${orderByProperty}>" ) } @@ -677,7 +715,7 @@ final case class SearchResponderV2Live( ) ) { throw BadRequestException( - s"Class <${resourcesInProjectGetRequestV2.resourceClass}> has no cardinality on property <${resourcesInProjectGetRequestV2.orderByProperty}>" + s"Class <${resourceClass}> has no cardinality on property <${orderByProperty}>" ) } @@ -722,12 +760,12 @@ final case class SearchResponderV2Live( // Do a SELECT prequery to get the IRIs of the requested page of resources. prequery = sparql.v2.txt .getResourcesByClassInProjectPrequery( - projectIri = resourcesInProjectGetRequestV2.projectIri.toString, + projectIri = projectIri.toString, resourceClassIri = internalClassIri, maybeOrderByProperty = maybeInternalOrderByPropertyIri, maybeOrderByValuePredicate = maybeOrderByValuePredicate, limit = appConfig.v2.resourcesSequence.resultsPerPage, - offset = resourcesInProjectGetRequestV2.page * appConfig.v2.resourcesSequence.resultsPerPage + offset = page * appConfig.v2.resourcesSequence.resultsPerPage ) sparqlSelectResponse <- triplestore.query(Select(prequery)) mainResourceIris: Seq[IRI] = sparqlSelectResponse.results.bindings.map(_.rowMap("resource")) @@ -736,7 +774,7 @@ final case class SearchResponderV2Live( // ConstructResponseUtilV2.makeTextValueContentV2. queryStandoff: Boolean = SchemaOptions.queryStandoffWithTextValues( targetSchema = ApiV2Complex, - schemaOptions = resourcesInProjectGetRequestV2.schemaOptions + schemaOptions = schemaAndOptions.options ) // Are there any matching resources? @@ -764,7 +802,7 @@ final case class SearchResponderV2Live( // separate resources and values mainResourcesAndValueRdfData = constructResponseUtilV2.splitMainResourcesAndValueRdfData( resourceRequestResponse, - resourcesInProjectGetRequestV2.requestingUser + requestingUser ) // If we're querying standoff, get XML-to standoff mappings. @@ -772,7 +810,7 @@ final case class SearchResponderV2Live( if (queryStandoff) { constructResponseUtilV2.getMappingsFromQueryResultsSeparated( mainResourcesAndValueRdfData.resources, - resourcesInProjectGetRequestV2.requestingUser + requestingUser ) } else { ZIO.succeed(Map.empty[IRI, MappingAndXSLTransformation]) @@ -787,8 +825,8 @@ final case class SearchResponderV2Live( queryStandoff = queryStandoff, versionDate = None, calculateMayHaveMoreResults = true, - targetSchema = resourcesInProjectGetRequestV2.targetSchema, - requestingUser = resourcesInProjectGetRequestV2.requestingUser + targetSchema = schemaAndOptions.schema, + requestingUser = requestingUser ) } yield readResourcesSequence } else { @@ -797,37 +835,20 @@ final case class SearchResponderV2Live( } yield apiResponse } - /** - * Performs a count query for a search for resources by their rdfs:label. - * - * @param searchValue the values to search for. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ - private def searchResourcesByLabelCountV2( + override def searchResourcesByLabelCountV2( searchValue: IRI, limitToProject: Option[IRI], limitToResourceClass: Option[SmartIri] ) = { - val searchPhrase: MatchStringWhileTyping = MatchStringWhileTyping(searchValue) + val searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence + val countSparql = SearchQueries.selectCountByLabel( + searchTerm = searchTerm, + limitToProject = limitToProject, + limitToResourceClass = limitToResourceClass.map(_.toString) + ) for { - countSparql <- - ZIO.attempt( - sparql.v2.txt - .searchResourceByLabel( - searchTerm = searchPhrase, - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass.map(_.toString), - limit = 1, - offset = 0, - countQuery = true - ) - ) - - countResponse <- triplestore.query(Select(countSparql)) + countResponse <- triplestore.query(countSparql) count <- // query response should contain one result with one row with the name "count" ZIO @@ -842,18 +863,7 @@ final case class SearchResponderV2Live( } yield ResourceCountV2(count.toInt) } - /** - * Performs a search for resources by their rdfs:label. - * - * @param searchValue the values to search for. - * @param offset the offset to be used for paging. - * @param limitToProject limit search to given project. - * @param limitToResourceClass limit search to given resource class. - * @param targetSchema the schema of the response. - * @param requestingUser the client making the request. - * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. - */ - private def searchResourcesByLabelV2( + override def searchResourcesByLabelV2( searchValue: String, offset: Int, limitToProject: Option[IRI], @@ -861,30 +871,18 @@ final case class SearchResponderV2Live( targetSchema: ApiV2Schema, requestingUser: UserADM ): Task[ReadResourcesSequenceV2] = { + val searchLimit = appConfig.v2.resourcesSequence.resultsPerPage + val searchOffset = offset * appConfig.v2.resourcesSequence.resultsPerPage + val searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence val searchResourceByLabelSparql = - limitToResourceClass match { - case None => - Construct( - sparql.v2.txt.searchResourceByLabel( - searchTerm = MatchStringWhileTyping(searchValue), - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass.map(_.toString), - limit = appConfig.v2.resourcesSequence.resultsPerPage, - offset = offset * appConfig.v2.resourcesSequence.resultsPerPage, - countQuery = false - ) - ) - case Some(cls) => - Construct( - sparql.v2.txt.searchResourceByLabelWithDefinedResourceClass( - searchTerm = MatchStringWhileTyping(searchValue), - limitToResourceClass = cls.toString, - limit = appConfig.v2.resourcesSequence.resultsPerPage, - offset = offset * appConfig.v2.resourcesSequence.resultsPerPage - ) - ) - } + SearchQueries.constructSearchByLabel( + searchTerm, + limitToResourceClass.map(_.toIri), + limitToProject, + searchLimit, + searchOffset + ) for { searchResourceByLabelResponse <- triplestore.query(searchResourceByLabelSparql).flatMap(_.asExtended) @@ -974,7 +972,7 @@ final case class SearchResponderV2Live( } else { // No. This must be a column resulting from GROUP_CONCAT, so use the GROUP_CONCAT // separator to concatenate the column values. - columnValues.mkString(AbstractPrequeryGenerator.groupConcatSeparator.toString) + columnValues.mkString(StringFormatter.INFORMATION_SEPARATOR_ONE.toString) } columnName -> mergedColumnValue @@ -1026,28 +1024,24 @@ object SearchResponderV2Live { queryTraverser <- ZIO.service[QueryTraverser] sparqlTransformerLive <- ZIO.service[OntologyInferencer] stringFormatter <- ZIO.service[StringFormatter] - mr <- ZIO.service[MessageRelay] typeInspectionRunner <- ZIO.service[GravsearchTypeInspectionRunner] inferenceOptimizationService <- ZIO.service[InferenceOptimizationService] iriConverter <- ZIO.service[IriConverter] constructTransformer <- ZIO.service[ConstructTransformer] - handler <- mr.subscribe( - new SearchResponderV2Live( - appConfig, - triplestoreService, - messageRelay, - constructResponseUtilV2, - ontologyCache, - standoffTagUtilV2, - queryTraverser, - sparqlTransformerLive, - typeInspectionRunner, - inferenceOptimizationService, - stringFormatter, - iriConverter, - constructTransformer - ) - ) - } yield handler + } yield new SearchResponderV2Live( + appConfig, + triplestoreService, + messageRelay, + constructResponseUtilV2, + ontologyCache, + standoffTagUtilV2, + queryTraverser, + sparqlTransformerLive, + typeInspectionRunner, + inferenceOptimizationService, + stringFormatter, + iriConverter, + constructTransformer + ) ) } diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala index 52bb1fdb9e..822da476dd 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala @@ -14,6 +14,7 @@ import java.util.UUID import dsp.errors.* import dsp.valueobjects.UuidUtil +import org.knora.webapi.SchemaAndOptions.apiV2SchemaWithOption import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageHandler @@ -32,7 +33,6 @@ import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.messages.v2.responder.ontologymessages.* import org.knora.webapi.messages.v2.responder.resourcemessages.* -import org.knora.webapi.messages.v2.responder.searchmessages.GravsearchRequestV2 import org.knora.webapi.messages.v2.responder.valuemessages.* import org.knora.webapi.responders.IriLocker import org.knora.webapi.responders.IriService @@ -76,6 +76,7 @@ final case class ValuesResponderV2Live( messageRelay: MessageRelay, permissionUtilADM: PermissionUtilADM, resourceUtilV2: ResourceUtilV2, + searchResponderV2: SearchResponderV2, triplestoreService: TriplestoreService, implicit val stringFormatter: StringFormatter ) extends ValuesResponderV2 @@ -1973,17 +1974,8 @@ final case class ValuesResponderV2Live( .toString() // Run the query. - parsedGravsearchQuery <- ZIO.succeed(GravsearchParser.parseQuery(gravsearchQuery)) - searchResponse <- - messageRelay - .ask[ReadResourcesSequenceV2]( - GravsearchRequestV2( - constructQuery = parsedGravsearchQuery, - targetSchema = ApiV2Complex, - schemaOptions = SchemaOptions.ForStandoffWithTextValues, - requestingUser = requestingUser - ) - ) + query <- ZIO.succeed(GravsearchParser.parseQuery(gravsearchQuery)) + searchResponse <- searchResponderV2.gravsearchV2(query, apiV2SchemaWithOption(MarkupAsXml), requestingUser) } yield searchResponse.toResource(resourceIri) /** @@ -2440,7 +2432,7 @@ final case class ValuesResponderV2Live( object ValuesResponderV2Live { val layer: URLayer[ - AppConfig & IriService & MessageRelay & PermissionUtilADM & ResourceUtilV2 & TriplestoreService & StringFormatter, + AppConfig & IriService & MessageRelay & PermissionUtilADM & ResourceUtilV2 & TriplestoreService & SearchResponderV2 & StringFormatter, ValuesResponderV2 ] = ZLayer.fromZIO { for { @@ -2450,8 +2442,9 @@ object ValuesResponderV2Live { pu <- ZIO.service[PermissionUtilADM] ru <- ZIO.service[ResourceUtilV2] ts <- ZIO.service[TriplestoreService] + sr <- ZIO.service[SearchResponderV2] sf <- ZIO.service[StringFormatter] - handler <- mr.subscribe(ValuesResponderV2Live(config, is, mr, pu, ru, ts, sf)) + handler <- mr.subscribe(ValuesResponderV2Live(config, is, mr, pu, ru, sr, ts, sf)) } yield handler } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala index cfdb8edadc..7ffb3996e8 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -21,6 +21,7 @@ import org.knora.webapi.core.MessageRelay import org.knora.webapi.http.directives.DSPApiDirectives import org.knora.webapi.http.version.ServerVersion import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.responders.v2.ValuesResponderV2 import org.knora.webapi.routing import org.knora.webapi.routing.admin.* @@ -33,6 +34,7 @@ import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.resourceinfo.api.ResourceInfoRoutes import org.knora.webapi.slice.resourceinfo.api.service.RestResourceInfoService import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.store.iiif.api.SipiService trait ApiRoutes { val routes: Route @@ -44,9 +46,7 @@ object ApiRoutes { * All routes composed together. */ val layer: URLayer[ - ActorSystem & AdminApiRoutes & AppConfig & AppRouter & IriConverter & KnoraProjectRepo & MessageRelay & - ProjectADMRestService & ProjectsEndpointsHandler & ResourceInfoRoutes & RestCardinalityService & - RestResourceInfoService & StringFormatter & ValuesResponderV2 & core.State & routing.Authenticator, + ActorSystem & AdminApiRoutes & AppConfig & AppRouter & IriConverter & KnoraProjectRepo & MessageRelay & ProjectADMRestService & ProjectsEndpointsHandler & ResourceInfoRoutes & RestCardinalityService & RestResourceInfoService & SearchResponderV2 & SipiService & StringFormatter & ValuesResponderV2 & core.State & routing.Authenticator, ApiRoutes ] = ZLayer { @@ -57,11 +57,10 @@ object ApiRoutes { adminApiRoutes <- ZIO.service[AdminApiRoutes] resourceInfoRoutes <- ZIO.service[ResourceInfoRoutes] routeData <- ZIO.succeed(KnoraRouteData(sys.system, router.ref, appConfig)) - runtime <- ZIO.runtime[ - AppConfig & IriConverter & KnoraProjectRepo & MessageRelay & ProjectADMRestService & - RestCardinalityService & RestResourceInfoService & StringFormatter & ValuesResponderV2 & - core.State & routing.Authenticator - ] + runtime <- + ZIO.runtime[ + AppConfig & IriConverter & KnoraProjectRepo & MessageRelay & ProjectADMRestService & RestCardinalityService & RestResourceInfoService & SearchResponderV2 & SipiService & StringFormatter & ValuesResponderV2 & core.State & routing.Authenticator + ] } yield ApiRoutesImpl(routeData, adminApiRoutes, resourceInfoRoutes, appConfig, runtime) } } @@ -80,7 +79,7 @@ private final case class ApiRoutesImpl( appConfig: AppConfig, implicit val runtime: Runtime[ AppConfig & IriConverter & KnoraProjectRepo & MessageRelay & ProjectADMRestService & RestCardinalityService & - RestResourceInfoService & StringFormatter & ValuesResponderV2 & core.State & routing.Authenticator + RestResourceInfoService & SearchResponderV2 & SipiService & StringFormatter & ValuesResponderV2 & core.State & routing.Authenticator ] ) extends ApiRoutes with AroundDirectives { diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala index 09f0b962e2..dae582e751 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/FilesRouteADM.scala @@ -5,7 +5,8 @@ package org.knora.webapi.routing.admin -import org.apache.pekko +import org.apache.pekko.http.scaladsl.server.Directives.* +import org.apache.pekko.http.scaladsl.server.Route import zio.* import dsp.errors.BadRequestException @@ -17,9 +18,7 @@ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM - -import pekko.http.scaladsl.server.Directives.* -import pekko.http.scaladsl.server.Route +import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode /** * Provides a routing function for the API that Sipi connects to. @@ -41,7 +40,8 @@ final case class FilesRouteADM( val requestMessage = for { requestingUser <- Authenticator.getUserADM(requestContext) projectID <- ZIO - .fromOption(stringFormatter.validateProjectShortcode(projectIDAndFile.head)) + .fromOption(projectIDAndFile.headOption) + .flatMap(Shortcode.from(_).toZIO) .orElseFail(BadRequestException(s"Invalid project ID: '${projectIDAndFile.head}'")) filename <- ZIO .fromOption(Iri.toSparqlEncodedString(projectIDAndFile(1))) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala index 4077f761a3..6910028b6e 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/GroupsRouteADM.scala @@ -21,6 +21,7 @@ import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM.* import org.knora.webapi.routing.RouteUtilZ +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import pekko.http.scaladsl.server.Directives.* import pekko.http.scaladsl.server.PathMatcher @@ -78,7 +79,7 @@ final case class GroupsRouteADM( val id: Validation[Throwable, Option[GroupIri]] = GroupIri.make(apiRequest.id) val name: Validation[Throwable, GroupName] = GroupName.make(apiRequest.name) val descriptions: Validation[Throwable, GroupDescriptions] = GroupDescriptions.make(apiRequest.descriptions) - val project: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.project) + val project: Validation[Throwable, ProjectIri] = ProjectIri.from(apiRequest.project) val status: Validation[Throwable, GroupStatus] = Validation.succeed(GroupStatus.make(apiRequest.status)) val selfjoin: Validation[Throwable, GroupSelfJoin] = GroupSelfJoin.make(apiRequest.selfjoin) val payloadValidation: Validation[Throwable, GroupCreatePayloadADM] = diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala index 59fb441171..a2990a28b7 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/CreateListItemsRouteADM.scala @@ -25,6 +25,7 @@ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM.* +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import pekko.http.scaladsl.server.Directives.* import pekko.http.scaladsl.server.PathMatcher @@ -54,7 +55,7 @@ final case class CreateListItemsRouteADM( post { entity(as[ListRootNodeCreateApiRequestADM]) { apiRequest => requestContext => val maybeId: Validation[Throwable, Option[ListIri]] = ListIri.make(apiRequest.id) - val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.make(apiRequest.projectIri) + val projectIri: Validation[Throwable, ProjectIri] = ProjectIri.from(apiRequest.projectIri) val maybeName: Validation[Throwable, Option[ListName]] = ListName.make(apiRequest.name) val labels: Validation[Throwable, Labels] = Labels.make(apiRequest.labels) val comments: Validation[Throwable, Comments] = Comments.make(apiRequest.comments) @@ -86,7 +87,7 @@ final case class CreateListItemsRouteADM( .when(iri != apiRequest.parentNodeIri) parentNodeIri = ListIri.make(apiRequest.parentNodeIri) id = ListIri.make(apiRequest.id) - projectIri = ProjectIri.make(apiRequest.projectIri) + projectIri = ProjectIri.from(apiRequest.projectIri) name = ListName.make(apiRequest.name) position = Position.make(apiRequest.position) labels = Labels.make(apiRequest.labels) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala index 051b8bb0ff..7a9653f1d9 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/lists/UpdateListItemsRouteADM.scala @@ -24,6 +24,7 @@ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.KnoraRoute import org.knora.webapi.routing.KnoraRouteData import org.knora.webapi.routing.RouteUtilADM.* +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import pekko.http.scaladsl.server.Directives.* import pekko.http.scaladsl.server.PathMatcher @@ -135,7 +136,7 @@ final case class UpdateListItemsRouteADM( val validatedPayload = for { _ <- ZIO.fail(BadRequestException("Route and payload listIri mismatch.")).when(iri != apiRequest.listIri) listIri = ListIri.make(apiRequest.listIri) - projectIri = ProjectIri.make(apiRequest.projectIri) + projectIri = ProjectIri.from(apiRequest.projectIri) hasRootNode = ListIri.make(apiRequest.hasRootNode) position = Position.make(apiRequest.position) name = ListName.make(apiRequest.name) diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala index 07e818b0c5..88ad9ac430 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ResourcesRouteV2.scala @@ -27,20 +27,21 @@ import org.knora.webapi.messages.ValuesValidator.arkTimestampToInstant import org.knora.webapi.messages.ValuesValidator.xsdDateTimeStampToInstant import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.messages.v2.responder.resourcemessages.* -import org.knora.webapi.messages.v2.responder.searchmessages.SearchResourcesByProjectAndClassRequestV2 import org.knora.webapi.messages.v2.responder.valuemessages.* +import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilZ import org.knora.webapi.slice.resourceinfo.api.service.RestResourceInfoService import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.store.iiif.api.SipiService /** * Provides a routing function for API v2 routes that deal with resources. */ final case class ResourcesRouteV2(appConfig: AppConfig)( private implicit val runtime: Runtime[ - AppConfig & Authenticator & StringFormatter & IriConverter & MessageRelay & RestResourceInfoService + AppConfig & Authenticator & SearchResponderV2 & SipiService & StringFormatter & IriConverter & MessageRelay & RestResourceInfoService ] ) extends LazyLogging { private val sipiConfig: Sipi = appConfig.sipi @@ -164,25 +165,26 @@ final case class ResourcesRouteV2(appConfig: AppConfig)( .orElseFail(BadRequestException(s"This route requires the request header ${RouteUtilV2.PROJECT_HEADER}")) val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) - val requestTask = for { + val response = for { maybeOrderByProperty <- getOrderByProperty resourceClass <- getResourceClass projectIri <- getProjectIri page <- getPage - targetSchema <- targetSchemaTask - schemaOptions <- RouteUtilV2.getSchemaOptions(requestContext) - requestingUser <- Authenticator.getUserADM(requestContext) - } yield SearchResourcesByProjectAndClassRequestV2( - projectIri, - resourceClass, - maybeOrderByProperty, - page, - targetSchema, - schemaOptions, - requestingUser - ) - - RouteUtilV2.runRdfRouteZ(requestTask, requestContext, targetSchemaTask) + targetSchema <- targetSchemaTask.zip(RouteUtilV2.getSchemaOptions(requestContext)).map { + case (schema, options) => SchemaAndOptions(schema, options) + } + requestingUser <- Authenticator.getUserADM(requestContext) + response <- SearchResponderV2.searchResourcesByProjectAndClassV2( + projectIri, + resourceClass, + maybeOrderByProperty, + page, + targetSchema, + requestingUser + ) + } yield response + + RouteUtilV2.completeResponse(response, requestContext, targetSchemaTask) } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala index 4e691ac527..b5c2f98429 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala @@ -5,11 +5,13 @@ package org.knora.webapi.routing.v2 -import org.apache.pekko +import org.apache.pekko.http.scaladsl.server.Directives.* +import org.apache.pekko.http.scaladsl.server.RequestContext +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.server.RouteResult import zio.* -import zio.metrics.* -import java.time.temporal.ChronoUnit +import scala.concurrent.Future import dsp.errors.BadRequestException import dsp.valueobjects.Iri @@ -20,22 +22,17 @@ import org.knora.webapi.messages.OntologyConstants import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.ValuesValidator import org.knora.webapi.messages.util.search.gravsearch.GravsearchParser -import org.knora.webapi.messages.v2.responder.KnoraResponseV2 -import org.knora.webapi.messages.v2.responder.searchmessages.* +import org.knora.webapi.responders.v2.ResourceCountV2 +import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.slice.resourceinfo.domain.IriConverter -import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException - -import pekko.http.scaladsl.server.Directives.* -import pekko.http.scaladsl.server.RequestContext -import pekko.http.scaladsl.server.Route /** * Provides a function for API routes that deal with search. */ final case class SearchRouteV2(searchValueMinLength: Int)( - private implicit val runtime: Runtime[AppConfig & Authenticator & IriConverter & MessageRelay] + private implicit val runtime: Runtime[AppConfig & Authenticator & IriConverter & SearchResponderV2 & MessageRelay] ) { private val LIMIT_TO_PROJECT = "limitToProject" @@ -139,21 +136,20 @@ final case class SearchRouteV2(searchValueMinLength: Int)( searchStr => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => val params: Map[String, String] = requestContext.request.uri.query().toMap - val requestTask = for { + val response = for { _ <- ensureIsNotFullTextSearch(searchStr) escapedSearchStr <- validateSearchString(searchStr) limitToProject <- getProjectFromParams(params) limitToResourceClass <- getResourceClassFromParams(params) limitToStandoffClass <- getStandoffClass(params) - user <- Authenticator.getUserADM(requestContext) - } yield FullTextSearchCountRequestV2( - escapedSearchStr, - limitToProject, - limitToResourceClass, - limitToStandoffClass, - user - ) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) + response <- SearchResponderV2.fulltextSearchCountV2( + escapedSearchStr, + limitToProject, + limitToResourceClass, + limitToStandoffClass + ) + } yield response + RouteUtilV2.completeResponse(response, requestContext) } } @@ -192,46 +188,41 @@ final case class SearchRouteV2(searchValueMinLength: Int)( limitToStandoffClass <- getStandoffClass(params) returnFiles = ValuesValidator.optionStringToBoolean(params.get(RETURN_FILES), fallback = false) requestingUser <- Authenticator.getUserADM(requestContext) - targetSchema <- targetSchemaTask - schemaOptions <- schemaOptionsTask - } yield FulltextSearchRequestV2( - searchValue = escapedSearchStr, - offset = offset, - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass, - limitToStandoffClass = limitToStandoffClass, - returnFiles = returnFiles, - requestingUser = requestingUser, - targetSchema = targetSchema, - schemaOptions = schemaOptions - ) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext, targetSchemaTask, schemaOptionsTask.map(Some(_))) + schemaAndOptions <- targetSchemaTask.zip(schemaOptionsTask).map { case (s, o) => SchemaAndOptions(s, o) } + response <- SearchResponderV2.fulltextSearchV2( + escapedSearchStr, + offset, + limitToProject, + limitToResourceClass, + limitToStandoffClass, + returnFiles, + schemaAndOptions, + requestingUser + ) + } yield response + RouteUtilV2.completeResponse(requestTask, requestContext, targetSchemaTask, schemaOptionsTask.map(Some(_))) } } private def gravsearchCountGet(): Route = - path("v2" / "searchextended" / "count" / Segment) { - gravsearchQuery => // Segment is a URL encoded string representing a Gravsearch query - get { requestContext => - val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) - val requestTask = Authenticator.getUserADM(requestContext).map(GravsearchCountRequestV2(constructQuery, _)) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } + path("v2" / "searchextended" / "count" / Segment) { query => + get(gravsearchCountV2(query, _)) } private def gravsearchCountPost(): Route = path("v2" / "searchextended" / "count") { - post { - entity(as[String]) { gravsearchQuery => requestContext => - { - val constructQuery = GravsearchParser.parseQuery(gravsearchQuery) - val requestTask = Authenticator.getUserADM(requestContext).map(GravsearchCountRequestV2(constructQuery, _)) - RouteUtilV2.runRdfRouteZ(requestTask, requestContext) - } - } - } + post(entity(as[String])(query => gravsearchCountV2(query, _))) } + private def gravsearchCountV2(query: String, ctx: RequestContext): Future[RouteResult] = { + val response: ZIO[SearchResponderV2 & Authenticator, Throwable, ResourceCountV2] = for { + user <- Authenticator.getUserADM(ctx) + gravsearch <- ZIO.attempt(GravsearchParser.parseQuery(query)) + response <- SearchResponderV2.gravsearchCountV2(gravsearch, user) + } yield response + RouteUtilV2.completeResponse(response, ctx) + } + private def gravsearchGet(): Route = path( "v2" / "searchextended" / Segment ) { query => // Segment is a URL encoded string representing a Gravsearch query @@ -242,27 +233,14 @@ final case class SearchRouteV2(searchValueMinLength: Int)( post(entity(as[String])(query => requestContext => gravsearch(query, requestContext))) } - private val gravsearchDuration = Metric.timer("gravsearch", ChronoUnit.MILLIS, Chunk.iterate(1.0, 17)(_ * 2)) - private val gravsearchDurationSummary = - Metric.summary("gravsearch_summary", 1.day, 100, 0.03d, Chunk(0.01, 0.1, 0.2, 0.5, 0.8, 0.9, 0.99)) - private val gravsearchFailCounter = Metric.counter("gravsearch_fail").fromConst(1) - private val gravsearchTimeoutCounter = Metric.counter("gravsearch_timeout").fromConst(1) - private def gravsearch(query: String, requestContext: RequestContext) = { val constructQuery = GravsearchParser.parseQuery(query) val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) val schemaOptionsTask = RouteUtilV2.getSchemaOptions(requestContext) val task = for { - start <- Clock.instant.map(_.toEpochMilli).map(_.toDouble) - targetSchema <- targetSchemaTask - requestingUser <- Authenticator.getUserADM(requestContext) - schemaOptions <- schemaOptionsTask - request = GravsearchRequestV2(constructQuery, targetSchema, schemaOptions, requestingUser) - response <- MessageRelay.ask[KnoraResponseV2](request).tapError { - case _: TriplestoreTimeoutException => ZIO.unit @@ gravsearchTimeoutCounter - case _ => ZIO.unit @@ gravsearchFailCounter - } @@ gravsearchDuration.trackDuration - _ <- Clock.instant.map(_.toEpochMilli).map(_.-(start)) @@ gravsearchDurationSummary + schemaAndOptions <- targetSchemaTask.zip(schemaOptionsTask).map { case (s, o) => SchemaAndOptions(s, o) } + user <- Authenticator.getUserADM(requestContext) + response <- SearchResponderV2.gravsearchV2(constructQuery, schemaAndOptions, user) } yield response RouteUtilV2.completeResponse(task, requestContext, targetSchemaTask, schemaOptionsTask.map(Some(_))) } @@ -272,13 +250,14 @@ final case class SearchRouteV2(searchValueMinLength: Int)( searchval => // TODO: if a space is encoded as a "+", this is not converted back to a space get { requestContext => val params: Map[String, String] = requestContext.request.uri.query().toMap - val requestMessage = for { + val response = for { searchString <- validateSearchString(searchval) limitToProject <- getProjectFromParams(params) limitToResourceClass <- getResourceClassFromParams(params) - user <- Authenticator.getUserADM(requestContext) - } yield SearchResourceByLabelCountRequestV2(searchString, limitToProject, limitToResourceClass, user) - RouteUtilV2.runRdfRouteZ(requestMessage, requestContext, RouteUtilV2.getOntologySchema(requestContext)) + response <- + SearchResponderV2.searchResourcesByLabelCountV2(searchString, limitToProject, limitToResourceClass) + } yield response + RouteUtilV2.completeResponse(response, requestContext, RouteUtilV2.getOntologySchema(requestContext)) } } @@ -288,22 +267,23 @@ final case class SearchRouteV2(searchValueMinLength: Int)( get { requestContext => val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) val params: Map[String, String] = requestContext.request.uri.query().toMap - val requestMessage = for { + val response = for { sparqlEncodedSearchString <- validateSearchString(searchval) offset <- getOffsetFromParams(params) limitToProject <- getProjectFromParams(params) limitToResourceClass <- getResourceClassFromParams(params) targetSchema <- targetSchemaTask requestingUser <- Authenticator.getUserADM(requestContext) - } yield SearchResourceByLabelRequestV2( - searchValue = sparqlEncodedSearchString, - offset = offset, - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass, - targetSchema = targetSchema, - requestingUser = requestingUser - ) - RouteUtilV2.runRdfRouteZ(requestMessage, requestContext, targetSchemaTask) + response <- SearchResponderV2.searchResourcesByLabelV2( + searchValue = sparqlEncodedSearchString, + offset = offset, + limitToProject = limitToProject, + limitToResourceClass = limitToResourceClass, + targetSchema = targetSchema, + requestingUser = requestingUser + ) + } yield response + RouteUtilV2.completeResponse(response, requestContext, targetSchemaTask) } } } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala index c495146ca7..eb09dc23d4 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/v2/ValuesRouteV2.scala @@ -5,7 +5,9 @@ package org.knora.webapi.routing.v2 -import org.apache.pekko +import org.apache.pekko.http.scaladsl.server.Directives.* +import org.apache.pekko.http.scaladsl.server.PathMatcher +import org.apache.pekko.http.scaladsl.server.Route import zio.* import dsp.errors.BadRequestException @@ -21,17 +23,14 @@ import org.knora.webapi.routing.Authenticator import org.knora.webapi.routing.RouteUtilV2 import org.knora.webapi.routing.RouteUtilZ import org.knora.webapi.slice.resourceinfo.domain.IriConverter - -import pekko.http.scaladsl.server.Directives.* -import pekko.http.scaladsl.server.PathMatcher -import pekko.http.scaladsl.server.Route +import org.knora.webapi.store.iiif.api.SipiService /** * Provides a routing function for API v2 routes that deal with values. */ final case class ValuesRouteV2()( private implicit val runtime: Runtime[ - AppConfig & Authenticator & IriConverter & StringFormatter & MessageRelay & ValuesResponderV2 + AppConfig & Authenticator & IriConverter & SipiService & StringFormatter & MessageRelay & ValuesResponderV2 ] ) { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala index 3f180730ef..95dee8d3bf 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/model/ProjectsEndpointsRequests.scala @@ -8,7 +8,6 @@ package org.knora.webapi.slice.admin.api.model import zio.json.DeriveJsonCodec import zio.json.JsonCodec -import dsp.valueobjects.Iri.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.* object ProjectsEndpointsRequests { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala index ecbbc9a92a..fef388b285 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala @@ -10,7 +10,6 @@ import zio.macros.accessible import dsp.errors.BadRequestException import dsp.errors.NotFoundException -import dsp.valueobjects.Iri.ProjectIri import dsp.valueobjects.RestrictedViewSize import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* import org.knora.webapi.messages.admin.responder.projectsmessages.* @@ -22,6 +21,7 @@ import org.knora.webapi.slice.admin.api.model.ProjectImportResponse import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectSetRestrictedViewSizeRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.admin.domain.model.KnoraProject.Status import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala index 715add74b2..dc47b1ed89 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/model/KnoraProject.scala @@ -14,12 +14,16 @@ import scala.util.matching.Regex import dsp.errors.ValidationException import dsp.valueobjects.Iri +import dsp.valueobjects.Iri.isProjectIri +import dsp.valueobjects.Iri.validateAndEscapeProjectIri +import dsp.valueobjects.IriErrorMessages +import dsp.valueobjects.UuidUtil import dsp.valueobjects.V2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.* import org.knora.webapi.slice.resourceinfo.domain.InternalIri case class KnoraProject( - id: InternalIri, + id: ProjectIri, shortname: Shortname, shortcode: Shortcode, longname: Option[Longname], @@ -32,6 +36,29 @@ case class KnoraProject( ) object KnoraProject { + final case class ProjectIri private (value: String) extends AnyVal + + object ProjectIri { + + implicit val codec: JsonCodec[ProjectIri] = + JsonCodec[String].transformOrFail(ProjectIri.from(_).toEitherWith(e => e.head.getMessage), _.value) + + def unsafeFrom(str: String): ProjectIri = from(str).fold(e => throw e.head, identity) + + def from(str: String): Validation[ValidationException, ProjectIri] = str match { + case str if str.isEmpty => + Validation.fail(ValidationException(IriErrorMessages.ProjectIriMissing)) + case str if !isProjectIri(str) => + Validation.fail(ValidationException(IriErrorMessages.ProjectIriInvalid)) + case str if UuidUtil.hasValidLength(str.split("/").last) && !UuidUtil.hasSupportedVersion(str) => + Validation.fail(ValidationException(IriErrorMessages.UuidVersionInvalid)) + case _ => + Validation + .fromOption(validateAndEscapeProjectIri(str)) + .mapError(_ => ValidationException(IriErrorMessages.ProjectIriInvalid)) + .map(ProjectIri(_)) + } + } final case class Shortcode private (value: String) extends AnyVal diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala index 17527a8844..c9d50b5787 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/KnoraProjectRepo.scala @@ -9,11 +9,11 @@ import zio.Task import dsp.valueobjects.RestrictedViewSize import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.model.KnoraProject.Shortcode import org.knora.webapi.slice.common.repo.service.Repository -import org.knora.webapi.slice.resourceinfo.domain.InternalIri -trait KnoraProjectRepo extends Repository[KnoraProject, InternalIri] { +trait KnoraProjectRepo extends Repository[KnoraProject, ProjectIri] { def findById(id: ProjectIdentifierADM): Task[Option[KnoraProject]] def findByShortcode(code: Shortcode): Task[Option[KnoraProject]] = findById(ProjectIdentifierADM.ShortcodeIdentifier(code)) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala index 5d2ea8aa16..85aca29840 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMService.scala @@ -83,7 +83,7 @@ final case class ProjectADMServiceLive( private def toKnoraProject(project: ProjectADM): KnoraProject = KnoraProject( - id = InternalIri.apply(project.id), + id = ProjectIri.unsafeFrom(project.id), shortname = Shortname.unsafeFrom(project.shortname), shortcode = Shortcode.unsafeFrom(project.shortcode), longname = project.longname.map(Longname.unsafeFrom), diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala index 69c8b4fc40..090008e232 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/repo/service/KnoraProjectRepoLive.scala @@ -34,7 +34,7 @@ final case class KnoraProjectRepoLive( private implicit val sf: StringFormatter ) extends KnoraProjectRepo { - override def findById(id: InternalIri): Task[Option[KnoraProject]] = + override def findById(id: ProjectIri): Task[Option[KnoraProject]] = findOneByQuery(sparql.admin.txt.getProjects(maybeIri = Some(id.value), None, None)) override def findById(id: ProjectIdentifierADM): Task[Option[KnoraProject]] = { @@ -62,37 +62,38 @@ final case class KnoraProjectRepoLive( } private def toKnoraProject(subjectPropsTuple: (SubjectV2, ConstructPredicateObjects)): Task[KnoraProject] = { - val projectIri = InternalIri(subjectPropsTuple._1.toString) - val propsMap = subjectPropsTuple._2 + val (subject, propertiesMap) = subjectPropsTuple for { + projectIri <- ProjectIri.from(subject.value).toZIO shortname <- mapper - .getSingleOrFail[StringLiteralV2](ProjectShortname, propsMap) + .getSingleOrFail[StringLiteralV2](ProjectShortname, propertiesMap) .flatMap(l => Shortname.from(l.value).toZIO) shortcode <- mapper - .getSingleOrFail[StringLiteralV2](ProjectShortcode, propsMap) + .getSingleOrFail[StringLiteralV2](ProjectShortcode, propertiesMap) .flatMap(l => Shortcode.from(l.value).toZIO) longname <- mapper - .getSingleOption[StringLiteralV2](ProjectLongname, propsMap) + .getSingleOption[StringLiteralV2](ProjectLongname, propertiesMap) .flatMap(optLit => ZIO.foreach(optLit)(l => Longname.from(l.value).toZIO)) description <- mapper - .getNonEmptyChunkOrFail[StringLiteralV2](ProjectDescription, propsMap) + .getNonEmptyChunkOrFail[StringLiteralV2](ProjectDescription, propertiesMap) .map(_.map(l => V2.StringLiteralV2(l.value, l.language))) .flatMap(ZIO.foreach(_)(Description.from(_).toZIO)) keywords <- mapper - .getList[StringLiteralV2](ProjectKeyword, propsMap) + .getList[StringLiteralV2](ProjectKeyword, propertiesMap) .flatMap(l => ZIO.foreach(l.map(_.value).sorted)(Keyword.from(_).toZIO)) logo <- mapper - .getSingleOption[StringLiteralV2](ProjectLogo, propsMap) + .getSingleOption[StringLiteralV2](ProjectLogo, propertiesMap) .flatMap(optLit => ZIO.foreach(optLit)(l => Logo.from(l.value).toZIO)) status <- mapper - .getSingleOrFail[BooleanLiteralV2](StatusProp, propsMap) + .getSingleOrFail[BooleanLiteralV2](StatusProp, propertiesMap) .map(l => Status.from(l.value)) selfjoin <- mapper - .getSingleOrFail[BooleanLiteralV2](HasSelfJoinEnabled, propsMap) + .getSingleOrFail[BooleanLiteralV2](HasSelfJoinEnabled, propertiesMap) .map(l => SelfJoin.from(l.value)) - ontologies <- mapper - .getList[IriLiteralV2]("http://www.knora.org/ontology/knora-admin#belongsToOntology", propsMap) - .map(_.map(l => InternalIri(l.value))) + ontologies <- + mapper + .getList[IriLiteralV2]("http://www.knora.org/ontology/knora-admin#belongsToOntology", propertiesMap) + .map(_.map(l => InternalIri(l.value))) } yield KnoraProject( projectIri, shortname, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/RestPermissionService.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/RestPermissionService.scala index 6a4cd38e4c..b5972671c2 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/RestPermissionService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/RestPermissionService.scala @@ -53,7 +53,7 @@ trait RestPermissionService { object RestPermissionService { def isActive(userADM: UserADM): Boolean = userADM.status def isSystemAdmin(user: UserADM): Boolean = user.permissions.isSystemAdmin - def isProjectAdmin(user: UserADM, project: KnoraProject): Boolean = user.permissions.isProjectAdmin(project.id) + def isProjectAdmin(user: UserADM, project: KnoraProject): Boolean = user.permissions.isProjectAdmin(project.id.value) def isSystemOrProjectAdmin(project: KnoraProject)(userADM: UserADM): Boolean = isSystemAdmin(userADM) || isProjectAdmin(userADM, project) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala index 2470b89fd6..f3b6ff2952 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/domain/service/OntologyRepo.scala @@ -11,6 +11,7 @@ import zio.macros.accessible import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.common.repo.service.Repository import org.knora.webapi.slice.resourceinfo.domain.InternalIri @@ -22,9 +23,8 @@ trait OntologyRepo extends Repository[ReadOntologyV2, InternalIri] { override def findAll(): Task[List[ReadOntologyV2]] def findByProject(project: KnoraProject): Task[List[ReadOntologyV2]] = findByProject(project.id) - def findByProject(projectId: InternalIri): Task[List[ReadOntologyV2]] - def findOntologyGraphsByProject(project: KnoraProject): Task[List[InternalIri]] = - findByProject(project).map(_.map(_.ontologyMetadata.ontologyIri.toInternalIri)) + + def findByProject(projectId: ProjectIri): Task[List[ReadOntologyV2]] def findClassBy(classIri: InternalIri): Task[Option[ReadClassInfoV2]] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala index 10dbb503b1..80034735ef 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/ontology/repo/service/OntologyRepoLive.scala @@ -14,6 +14,7 @@ import scala.annotation.tailrec import org.knora.webapi.messages.SmartIri import org.knora.webapi.messages.v2.responder.ontologymessages.ReadClassInfoV2 import org.knora.webapi.messages.v2.responder.ontologymessages.ReadOntologyV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.ontology.domain.service.OntologyRepo import org.knora.webapi.slice.ontology.repo.model.OntologyCacheData import org.knora.webapi.slice.resourceinfo.domain.InternalIri @@ -45,8 +46,8 @@ final case class OntologyRepoLive(private val converter: IriConverter, private v override def findAll(): Task[List[ReadOntologyV2]] = getCache.map(_.ontologies.values.toList) - override def findByProject(projectId: InternalIri): Task[List[ReadOntologyV2]] = - smartIriMapCache(projectId)(findByProject) + override def findByProject(projectId: ProjectIri): Task[List[ReadOntologyV2]] = + smartIriMapCache(InternalIri(projectId.value))(findByProject) private def findByProject(projectIri: SmartIri, cache: OntologyCacheData): List[ReadOntologyV2] = cache.ontologies.values.filter(_.ontologyMetadata.projectIri.contains(projectIri)).toList diff --git a/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala index 6ecd1b817c..edc8cb5256 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/cache/impl/CacheServiceInMemImpl.scala @@ -8,7 +8,6 @@ package org.knora.webapi.store.cache.impl import zio.* import zio.stm.* -import dsp.valueobjects.Iri import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* @@ -18,6 +17,7 @@ import org.knora.webapi.messages.admin.responder.usersmessages.UserIdentifierTyp import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusOK import org.knora.webapi.messages.store.cacheservicemessages.CacheServiceStatusResponse import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.store.cache.api.CacheService import org.knora.webapi.store.cache.api.EmptyKey import org.knora.webapi.store.cache.api.EmptyValue @@ -133,7 +133,7 @@ case class CacheServiceInMemImpl( * @param iri the project's IRI * @return an optional [[ProjectADM]]. */ - def getProjectByIri(iri: Iri.ProjectIri) = projects.get(iri.value).commit + def getProjectByIri(iri: ProjectIri) = projects.get(iri.value).commit /** * Retrieves the project by the SHORTNAME. diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFRequestMessageHandler.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFRequestMessageHandler.scala index 01350b3263..fa15ffc941 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFRequestMessageHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/IIIFRequestMessageHandler.scala @@ -11,22 +11,20 @@ import org.knora.webapi.core.MessageHandler import org.knora.webapi.core.MessageRelay import org.knora.webapi.messages.ResponderRequest import org.knora.webapi.messages.store.sipimessages.DeleteTemporaryFileRequest -import org.knora.webapi.messages.store.sipimessages.GetFileMetadataRequest import org.knora.webapi.messages.store.sipimessages.IIIFRequest import org.knora.webapi.messages.store.sipimessages.IIIFServiceGetStatus import org.knora.webapi.messages.store.sipimessages.MoveTemporaryFileToPermanentStorageRequest import org.knora.webapi.messages.store.sipimessages.SipiGetTextFileRequest -import org.knora.webapi.store.iiif.api.IIIFService +import org.knora.webapi.store.iiif.api.SipiService trait IIIFRequestMessageHandler extends MessageHandler -final case class IIIFRequestMessageHandlerLive(iiifService: IIIFService) extends IIIFRequestMessageHandler { +final case class IIIFRequestMessageHandlerLive(iiifService: SipiService) extends IIIFRequestMessageHandler { override def isResponsibleFor(message: ResponderRequest): Boolean = message.isInstanceOf[IIIFRequest] override def handle(message: ResponderRequest): Task[Any] = message match { - case req: GetFileMetadataRequest => iiifService.getFileMetadata(req) case req: MoveTemporaryFileToPermanentStorageRequest => iiifService.moveTemporaryFileToPermanentStorage(req) case req: DeleteTemporaryFileRequest => iiifService.deleteTemporaryFile(req) case req: SipiGetTextFileRequest => iiifService.getTextFileRequest(req) @@ -36,10 +34,10 @@ final case class IIIFRequestMessageHandlerLive(iiifService: IIIFService) extends } object IIIFRequestMessageHandlerLive { - val layer: URLayer[IIIFService & MessageRelay, IIIFRequestMessageHandler] = ZLayer.fromZIO { + val layer: URLayer[SipiService & MessageRelay, IIIFRequestMessageHandler] = ZLayer.fromZIO { for { mr <- ZIO.service[MessageRelay] - is <- ZIO.service[IIIFService] + is <- ZIO.service[SipiService] handler <- mr.subscribe(IIIFRequestMessageHandlerLive(is)) } yield handler } diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala deleted file mode 100644 index 7d6491e2d5..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/IIIFService.scala +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.store.iiif.api - -import zio.* -import zio.macros.accessible -import zio.nio.file.Path - -import org.knora.webapi.messages.admin.responder.usersmessages.UserADM -import org.knora.webapi.messages.store.sipimessages.DeleteTemporaryFileRequest -import org.knora.webapi.messages.store.sipimessages.GetFileMetadataRequest -import org.knora.webapi.messages.store.sipimessages.GetFileMetadataResponse -import org.knora.webapi.messages.store.sipimessages.IIIFServiceStatusResponse -import org.knora.webapi.messages.store.sipimessages.MoveTemporaryFileToPermanentStorageRequest -import org.knora.webapi.messages.store.sipimessages.SipiGetTextFileRequest -import org.knora.webapi.messages.store.sipimessages.SipiGetTextFileResponse -import org.knora.webapi.messages.v2.responder.SuccessResponseV2 -import org.knora.webapi.slice.admin.domain.service.Asset - -@accessible -trait IIIFService { - - /** - * Asks Sipi for metadata about a file, served from the 'knora.json' route. - * - * @param getFileMetadataRequest the request. - * @return a [[GetFileMetadataResponse]] containing the requested metadata. - */ - def getFileMetadata(getFileMetadataRequest: GetFileMetadataRequest): Task[GetFileMetadataResponse] - - /** - * Asks Sipi to move a file from temporary storage to permanent storage. - * - * @param moveTemporaryFileToPermanentStorageRequestV2 the request. - * @return a [[SuccessResponseV2]]. - */ - def moveTemporaryFileToPermanentStorage( - moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest - ): Task[SuccessResponseV2] - - /** - * Asks Sipi to delete a temporary file. - * - * @param deleteTemporaryFileRequestV2 the request. - * @return a [[SuccessResponseV2]]. - */ - def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): Task[SuccessResponseV2] - - /** - * Asks Sipi for a text file used internally by Knora. - * - * @param textFileRequest the request message. - */ - def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] - - /** - * Tries to access the IIIF Service. - */ - def getStatus(): Task[IIIFServiceStatusResponse] - - /** - * Downloads an asset from Sipi. - * @param asset The asset to download. - * @param targetDir The target directory in which the asset should be stored. - * @param user The user who is downloading the asset. - * @return The path to the downloaded asset. If the asset could not be downloaded, [[None]] is returned. - */ - def downloadAsset(asset: Asset, targetDir: Path, user: UserADM): Task[Option[Path]] -} diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala new file mode 100644 index 0000000000..f7db7e776a --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/api/SipiService.scala @@ -0,0 +1,110 @@ +/* + * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.store.iiif.api + +import zio.* +import zio.json.DeriveJsonDecoder +import zio.json.JsonDecoder +import zio.macros.accessible +import zio.nio.file.Path + +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.messages.store.sipimessages.* +import org.knora.webapi.messages.v2.responder.SuccessResponseV2 +import org.knora.webapi.slice.admin.domain.service.Asset +import org.knora.webapi.store.iiif.errors.SipiException + +/** + * Represents file metadata returned by Sipi. + * + * @param originalFilename the file's original filename, if known. + * @param originalMimeType the file's original MIME type. + * @param internalMimeType the file's internal MIME type. + * @param width the file's width in pixels, if applicable. + * @param height the file's height in pixels, if applicable. + * @param numpages the number of pages in the file, if applicable. + * @param duration the duration of the file in seconds, if applicable. + */ +case class FileMetadataSipiResponse( + originalFilename: Option[String], + originalMimeType: Option[String], + internalMimeType: String, + width: Option[Int], + height: Option[Int], + numpages: Option[Int], + duration: Option[BigDecimal], + fps: Option[BigDecimal] +) { + if (originalFilename.contains("")) { + throw SipiException(s"Sipi returned an empty originalFilename") + } + + if (originalMimeType.contains("")) { + throw SipiException(s"Sipi returned an empty originalMimeType") + } +} + +object FileMetadataSipiResponse { + // Because Sipi returns JSON Numbers which are whole numbers but not a valid Scala Int, e.g. `width: 1920.0`, we need to + // use a custom decoder for Int. See also https://github.com/zio/zio-json/issues/1049#issuecomment-1814108354 + implicit val anyWholeNumber: JsonDecoder[Int] = JsonDecoder[Double].mapOrFail { d => + val i = d.toInt + if (d == i.toDouble) { Right(i) } + else { Left("32-bit int expected") } + } + implicit val decoder: JsonDecoder[FileMetadataSipiResponse] = DeriveJsonDecoder.gen[FileMetadataSipiResponse] +} + +@accessible +trait SipiService { + + /** + * Asks Sipi for metadata about a file, served from the 'knora.json' route. + * + * @param filePath the path to the file. + * @return a [[FileMetadataSipiResponse]] containing the requested metadata. + */ + def getFileMetadata(filePath: String): Task[FileMetadataSipiResponse] + + /** + * Asks Sipi to move a file from temporary storage to permanent storage. + * + * @param moveTemporaryFileToPermanentStorageRequestV2 the request. + * @return a [[SuccessResponseV2]]. + */ + def moveTemporaryFileToPermanentStorage( + moveTemporaryFileToPermanentStorageRequestV2: MoveTemporaryFileToPermanentStorageRequest + ): Task[SuccessResponseV2] + + /** + * Asks Sipi to delete a temporary file. + * + * @param deleteTemporaryFileRequestV2 the request. + * @return a [[SuccessResponseV2]]. + */ + def deleteTemporaryFile(deleteTemporaryFileRequestV2: DeleteTemporaryFileRequest): Task[SuccessResponseV2] + + /** + * Asks Sipi for a text file used internally by Knora. + * + * @param textFileRequest the request message. + */ + def getTextFileRequest(textFileRequest: SipiGetTextFileRequest): Task[SipiGetTextFileResponse] + + /** + * Tries to access the IIIF Service. + */ + def getStatus(): Task[IIIFServiceStatusResponse] + + /** + * Downloads an asset from Sipi. + * @param asset The asset to download. + * @param targetDir The target directory in which the asset should be stored. + * @param user The user who is downloading the asset. + * @return The path to the downloaded asset. If the asset could not be downloaded, [[None]] is returned. + */ + def downloadAsset(asset: Asset, targetDir: Path, user: UserADM): Task[Option[Path]] +} diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala deleted file mode 100644 index 5e74dc1387..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/domain/SipiKnoraJsonResponse.scala +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.knora.webapi.store.iiif.domain - -import org.apache.pekko -import spray.json.DefaultJsonProtocol -import spray.json.RootJsonFormat - -import org.knora.webapi.store.iiif.errors.SipiException - -import pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport - -/** - * Represents a response from Sipi's `knora.json` route. - * - * @param originalFilename the file's original filename, if known. - * @param originalMimeType the file's original MIME type. - * @param internalMimeType the file's internal MIME type. - * @param width the file's width in pixels, if applicable. - * @param height the file's height in pixels, if applicable. - * @param numpages the number of pages in the file, if applicable. - * @param duration the duration of the file in seconds, if applicable. - * @param fps the frame rate of the file, if applicable. - */ -final case class SipiKnoraJsonResponse( - originalFilename: Option[String], - originalMimeType: Option[String], - internalMimeType: String, - width: Option[Int], - height: Option[Int], - numpages: Option[Int], - duration: Option[BigDecimal], - fps: Option[BigDecimal] -) { - if (originalFilename.contains("")) { - throw SipiException(s"Sipi returned an empty originalFilename") - } - - if (originalMimeType.contains("")) { - throw SipiException(s"Sipi returned an empty originalMimeType") - } -} - -object SipiKnoraJsonResponseProtocol extends SprayJsonSupport with DefaultJsonProtocol { - implicit val sipiKnoraJsonResponseFormat: RootJsonFormat[SipiKnoraJsonResponse] = jsonFormat8(SipiKnoraJsonResponse) -} diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala similarity index 90% rename from webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala rename to webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala index 8b923856c3..1c17ed960d 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/IIIFServiceSipiImpl.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/impl/SipiServiceLive.scala @@ -13,11 +13,7 @@ import org.apache.http.HttpResponse import org.apache.http.NameValuePair import org.apache.http.client.config.RequestConfig import org.apache.http.client.entity.UrlEncodedFormEntity -import org.apache.http.client.methods.CloseableHttpResponse -import org.apache.http.client.methods.HttpDelete -import org.apache.http.client.methods.HttpGet -import org.apache.http.client.methods.HttpPost -import org.apache.http.client.methods.HttpUriRequest +import org.apache.http.client.methods.* import org.apache.http.client.protocol.HttpClientContext import org.apache.http.config.SocketConfig import org.apache.http.impl.client.CloseableHttpClient @@ -27,6 +23,7 @@ import org.apache.http.message.BasicNameValuePair import org.apache.http.util.EntityUtils import spray.json.* import zio.* +import zio.json.DecoderOps import zio.nio.file.Path import java.net.URI @@ -42,8 +39,8 @@ import org.knora.webapi.messages.v2.responder.SuccessResponseV2 import org.knora.webapi.routing.Jwt import org.knora.webapi.routing.JwtService import org.knora.webapi.slice.admin.domain.service.Asset -import org.knora.webapi.store.iiif.api.IIIFService -import org.knora.webapi.store.iiif.domain.* +import org.knora.webapi.store.iiif.api.FileMetadataSipiResponse +import org.knora.webapi.store.iiif.api.SipiService import org.knora.webapi.store.iiif.errors.SipiException import org.knora.webapi.util.SipiUtil import org.knora.webapi.util.ZScopedJavaIoStreams @@ -55,11 +52,11 @@ import org.knora.webapi.util.ZScopedJavaIoStreams * @param jwtService The JWT Service to handle JWT Tokens * @param httpClient The HTTP Client */ -final case class IIIFServiceSipiImpl( +final case class SipiServiceLive( private val sipiConfig: Sipi, private val jwtService: JwtService, private val httpClient: CloseableHttpClient -) extends IIIFService { +) extends SipiService { private object SipiRoutes { def file(asset: Asset): UIO[URI] = makeUri(s"${assetBase(asset)}/file") @@ -72,28 +69,16 @@ final case class IIIFServiceSipiImpl( /** * Asks Sipi for metadata about a file, served from the 'knora.json' route. * - * @param getFileMetadataRequest the request. - * @return a [[GetFileMetadataResponse]] containing the requested metadata. + * @param filePath the path to the file. + * @return a [[FileMetadataSipiResponse]] containing the requested metadata. */ - def getFileMetadata(getFileMetadataRequest: GetFileMetadataRequest): Task[GetFileMetadataResponse] = { - import SipiKnoraJsonResponseProtocol.* - - for { - url <- ZIO.succeed(sipiConfig.internalBaseUrl + getFileMetadataRequest.filePath + "/knora.json") - request <- ZIO.succeed(new HttpGet(url)) - sipiResponseStr <- doSipiRequest(request) - sipiResponse <- ZIO.attempt(sipiResponseStr.parseJson.convertTo[SipiKnoraJsonResponse]) - } yield GetFileMetadataResponse( - originalFilename = sipiResponse.originalFilename, - originalMimeType = sipiResponse.originalMimeType, - internalMimeType = sipiResponse.internalMimeType, - width = sipiResponse.width, - height = sipiResponse.height, - pageCount = sipiResponse.numpages, - duration = sipiResponse.duration, - fps = sipiResponse.fps - ) - } + override def getFileMetadata(filePath: String): Task[FileMetadataSipiResponse] = + doSipiRequest(new HttpGet(sipiConfig.internalBaseUrl + filePath + "/knora.json")) + .flatMap(bodyStr => + ZIO + .fromEither(bodyStr.fromJson[FileMetadataSipiResponse]) + .mapError(e => SipiException(s"Invalid response from Sipi: $e, $bodyStr")) + ) /** * Asks Sipi to move a file from temporary storage to permanent storage. @@ -351,7 +336,7 @@ final case class IIIFServiceSipiImpl( .as(targetFile) } -object IIIFServiceSipiImpl { +object SipiServiceLive { /** * Acquires a configured httpClient, backed by a connection pool, @@ -404,12 +389,12 @@ object IIIFServiceSipiImpl { private def release(httpClient: CloseableHttpClient): UIO[Unit] = ZIO.attemptBlocking(httpClient.close()).logError.ignore <* ZIO.logInfo(">>> Release Sipi IIIF Service <<<") - val layer: URLayer[AppConfig & JwtService, IIIFService] = + val layer: URLayer[AppConfig & JwtService, SipiService] = ZLayer.scoped { for { config <- ZIO.serviceWith[AppConfig](_.sipi) jwtService <- ZIO.service[JwtService] httpClient <- ZIO.acquireRelease(acquire(config))(release) - } yield IIIFServiceSipiImpl(config, jwtService, httpClient) + } yield SipiServiceLive(config, jwtService, httpClient) } } diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabel.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabel.scala.txt deleted file mode 100644 index 09d755d969..0000000000 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabel.scala.txt +++ /dev/null @@ -1,116 +0,0 @@ -@* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - *@ - -@import org.knora.webapi.IRI -@import dsp.errors.SparqlGenerationException -@import org.knora.webapi.util.ApacheLuceneSupport.MatchStringWhileTyping - -@* - * Performs a search for resources by their label using SPARQL w/o inference. - * - * @param searchTerm search terms. - * @param limitToProject limit search to the given project. - * @param limitToResourceClass limit search to given resource class. - * @param limit maximum amount of resources to be returned. - * @param offset offset to be used for paging. - * @param countQuery indicates whether it is a count query or the actual resources should be returned. - *@ -@(searchTerm: MatchStringWhileTyping, - limitToProject: Option[IRI], - limitToResourceClass: Option[IRI], - limit: Int, - offset: Int, - countQuery: Boolean) - -PREFIX rdfs: -PREFIX knora-base: - -@if(!countQuery) { - CONSTRUCT { - ?resource rdfs:label ?label ; - a knora-base:Resource ; - knora-base:isMainResource true ; - knora-base:isDeleted false ; - a ?resourceType ; - knora-base:attachedToUser ?resourceCreator ; - knora-base:hasPermissions ?resourcePermissions ; - knora-base:attachedToProject ?resourceProject ; - knora-base:creationDate ?creationDate ; - knora-base:lastModificationDate ?lastModificationDate . - - # include this inferred information in the results, needed to identify value properties - ?resource knora-base:hasValue ?valueObject ; - ?resourceValueProperty ?valueObject . - ?valueObject ?valueObjectProperty ?valueObjectValue . - - } WHERE { - { - { - @{ - org.knora.webapi.messages.twirl.queries.sparql.v2.txt.searchResourceByLabelSubQuery( - searchTerm = searchTerm, - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass, - limit = limit, - offset = offset, - countQuery = countQuery) - } - } - - ?resource a ?resourceType ; - knora-base:attachedToUser ?resourceCreator ; - knora-base:hasPermissions ?resourcePermissions ; - knora-base:attachedToProject ?resourceProject ; - knora-base:creationDate ?creationDate ; - rdfs:label ?label . - - OPTIONAL { - ?resource knora-base:lastModificationDate ?lastModificationDate . - } - } - # there might be resources that have neither values nor links - UNION { - { - @{ - org.knora.webapi.messages.twirl.queries.sparql.v2.txt.searchResourceByLabelSubQuery( - searchTerm = searchTerm, - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass, - limit = limit, - offset = offset, - countQuery = countQuery) - } - } - - ?resource ?resourceValueProperty ?valueObject . - ?resourceValueProperty rdfs:subPropertyOf* knora-base:hasValue . - - ?valueObject a ?valueObjectType ; - ?valueObjectProperty ?valueObjectValue . - - ?valueObjectType rdfs:subClassOf* knora-base:Value . - - FILTER(?valueObjectType != knora-base:LinkValue) - - FILTER NOT EXISTS { - ?valueObject knora-base:isDeleted true . - } - - FILTER NOT EXISTS { - ?valueObjectValue a knora-base:StandoffTag . - } - } - } -} else { - @{ - org.knora.webapi.messages.twirl.queries.sparql.v2.txt.searchResourceByLabelSubQuery( - searchTerm = searchTerm, - limitToProject = limitToProject, - limitToResourceClass = limitToResourceClass, - limit = limit, - offset = offset, - countQuery = countQuery) - } -} diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabelSubQuery.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabelSubQuery.scala.txt deleted file mode 100644 index 2675ac80ef..0000000000 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabelSubQuery.scala.txt +++ /dev/null @@ -1,57 +0,0 @@ -@* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - *@ - -@import org.knora.webapi.IRI -@import dsp.errors.SparqlGenerationException -@import org.knora.webapi.util.ApacheLuceneSupport.MatchStringWhileTyping - -@* - * Select subquery for a search for resources by their label. - * - * @param searchTerm search terms. - * @param limitToProject limit search to the given project. - * @param limitToResourceClass limit search to given resource class. - * @param limit maximum amount of resources to be returned. - * @param offset offset to be used for paging. - * @param countQuery indicates whether it is a count query or the actual resources should be returned. - *@ - - @(searchTerm: MatchStringWhileTyping, - limitToProject: Option[IRI], - limitToResourceClass: Option[IRI], - limit: Int, - offset: Int, - countQuery: Boolean) - -@if(!countQuery) { - SELECT DISTINCT ?resource ?label -} else { - SELECT (count(distinct ?resource) as ?count) -} -WHERE { - ?resource (rdfs:label "@searchTerm.generateLiteralForLuceneIndexWithoutExactSequence") . - - ?resource a ?resourceClass ; - rdfs:label ?label . - - ?resourceClass rdfs:subClassOf* knora-base:Resource . - - @if(limitToResourceClass.nonEmpty) { - ?resourceClass rdfs:subClassOf* <@limitToResourceClass.get> . - } - - @if(limitToProject.nonEmpty) { - ?resource knora-base:attachedToProject <@limitToProject.get> - } - - FILTER NOT EXISTS { - ?resource knora-base:isDeleted true . - } -} -@if(!countQuery) { -ORDER BY ?resource @* Needed for paging: order needs to be deterministic *@ -} -LIMIT @limit -OFFSET @offset diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabelWithDefinedResourceClass.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabelWithDefinedResourceClass.scala.txt deleted file mode 100644 index 20cd07d0e5..0000000000 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/v2/searchResourceByLabelWithDefinedResourceClass.scala.txt +++ /dev/null @@ -1,90 +0,0 @@ -@* - * Copyright © 2021 - 2023 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. - * SPDX-License-Identifier: Apache-2.0 - *@ - -@import org.knora.webapi.IRI -@import org.knora.webapi.util.ApacheLuceneSupport.MatchStringWhileTyping - -@* - * Performs a search for resources by their label using SPARQL. - * - * @param searchTerm search terms. - * @param limitToResourceClass limit search to given resource class. - * @param limit maximum amount of resources to be returned. - * @param offset offset to be used for paging. - *@ -@(searchTerm: MatchStringWhileTyping, - limitToResourceClass: IRI, - limit: Int, - offset: Int) - -PREFIX rdfs: -PREFIX knora-base: - -CONSTRUCT { - ?resource rdfs:label ?label ; - a knora-base:Resource ; - knora-base:isMainResource true ; - knora-base:isDeleted false ; - a ?resourceType ; - knora-base:attachedToUser ?resourceCreator ; - knora-base:hasPermissions ?resourcePermissions ; - knora-base:attachedToProject ?resourceProject ; - knora-base:creationDate ?creationDate ; - knora-base:lastModificationDate ?lastModificationDate . - - ?resource knora-base:hasValue ?valueObject ; - ?resourceValueProperty ?valueObject . - ?valueObject ?valueObjectProperty ?valueObjectValue . -} WHERE { - { - SELECT DISTINCT ?resource ?label - WHERE { - ?resource (rdfs:label "@searchTerm.generateLiteralForLuceneIndexWithoutExactSequence") . - - ?resource a ?resourceClass ; - rdfs:label ?label . - ?resourceClass rdfs:subClassOf* <@limitToResourceClass> . - - FILTER NOT EXISTS { - ?resource knora-base:isDeleted true . - } - } - ORDER BY ?resource - LIMIT @limit - OFFSET @offset - } - - ?resource a ?resourceType ; - knora-base:attachedToUser ?resourceCreator ; - knora-base:hasPermissions ?resourcePermissions ; - knora-base:attachedToProject ?resourceProject ; - knora-base:creationDate ?creationDate ; - rdfs:label ?label . - - OPTIONAL { - ?resource knora-base:lastModificationDate ?lastModificationDate . - } - - OPTIONAL { - ?resource ?resourceValueProperty ?valueObject . - ?resourceValueProperty rdfs:subPropertyOf* knora-base:hasValue . - - ?valueObject a ?valueObjectType ; - ?valueObjectProperty ?valueObjectValue . - - ?valueObjectType rdfs:subClassOf* knora-base:Value . - - FILTER(?valueObjectType != knora-base:LinkValue) - - FILTER NOT EXISTS { - ?valueObject knora-base:isDeleted true . - } - - FILTER NOT EXISTS { - ?valueObjectValue a knora-base:StandoffTag . - } - } - -} diff --git a/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala b/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala index 27f7007304..a574e87c4a 100644 --- a/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala +++ b/webapi/src/test/scala/dsp/valueobjects/IriSpec.scala @@ -43,7 +43,7 @@ object IriSpec extends ZIOSpecDefault { val uuidVersion3 = fromIri(userIriWithUUIDVersion3) val supportedUuid = fromIri(validUserIri) - def spec: Spec[Any, Throwable] = groupIriTest + listIriTest + projectIriTest + uuidTest + roleIriTest + userIriTest + def spec: Spec[Any, Throwable] = groupIriTest + listIriTest + uuidTest + roleIriTest + userIriTest private val groupIriTest = suite("IriSpec - GroupIri")( test("pass an empty value and return an error") { @@ -133,59 +133,6 @@ object IriSpec extends ZIOSpecDefault { } ) - private val projectIriTest = suite("IriSpec - ProjectIri")( - test("pass an empty value and return an error") { - assertTrue( - ProjectIri.make("") == Validation.fail(ValidationException(IriErrorMessages.ProjectIriMissing)), - ProjectIri.make(Some("")) == Validation.fail(ValidationException(IriErrorMessages.ProjectIriMissing)) - ) - }, - test("pass an invalid value and return an error") { - assertTrue( - ProjectIri.make(invalidIri) == Validation.fail(ValidationException(IriErrorMessages.ProjectIriInvalid)), - ProjectIri.make(Some(invalidIri)) == Validation.fail(ValidationException(IriErrorMessages.ProjectIriInvalid)) - ) - }, - test("pass an invalid IRI containing unsupported UUID version and return an error") { - assertTrue( - ProjectIri.make(projectIriWithUUIDVersion3) == Validation.fail( - ValidationException(IriErrorMessages.UuidVersionInvalid) - ), - ProjectIri.make(Some(projectIriWithUUIDVersion3)) == Validation.fail( - ValidationException(IriErrorMessages.UuidVersionInvalid) - ) - ) - }, - test("pass an invalid IRI containing the shortcode and return an error") { - assertTrue( - ProjectIri.make(invalidIri) == Validation.fail(ValidationException(IriErrorMessages.ProjectIriInvalid)) - ) - }, - test("pass a valid project IRI and successfully create value object") { - def makeProjectIri(iri: String) = ProjectIri.make(iri) - val maybeProjectIri = ProjectIri.make(Some(validProjectIri)) - - (for { - iri <- makeProjectIri(validProjectIri) - iri2 <- makeProjectIri(systemProject) - iri3 <- makeProjectIri(defaultSharedOntologiesProject) - beolIri <- makeProjectIri(beolProjectIri) - maybeIri <- maybeProjectIri - } yield assertTrue( - iri.value == validProjectIri, - iri2.value == systemProject, - iri3.value == defaultSharedOntologiesProject, - beolIri.value == beolProjectIri, - maybeIri.get == iri - )).toZIO - }, - test("successfully validate passing None") { - assertTrue( - ProjectIri.make(None) == Validation.succeed(None) - ) - } - ) - private val uuidTest = suite("IriSpec - Base64Uuid")( test("pass an empty value and return an error") { assertTrue(Base64Uuid.make("") == Validation.fail(ValidationException(IriErrorMessages.UuidMissing))) diff --git a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala index cd6324c20c..8affb403eb 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestDataFactory.scala @@ -7,12 +7,10 @@ package org.knora.webapi import zio.NonEmptyChunk -import dsp.valueobjects.Iri.* import dsp.valueobjects.V2 import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.* import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.* -import org.knora.webapi.slice.resourceinfo.domain.InternalIri /** * Helps in creating value objects for tests. @@ -20,7 +18,7 @@ import org.knora.webapi.slice.resourceinfo.domain.InternalIri object TestDataFactory { val someProject = KnoraProject( - InternalIri("http://rdfh.ch/projects/0001"), + ProjectIri.unsafeFrom("http://rdfh.ch/projects/0001"), Shortname.unsafeFrom("shortname"), Shortcode.unsafeFrom("0001"), None, @@ -49,6 +47,6 @@ object TestDataFactory { def projectIri(iri: String): ProjectIri = ProjectIri - .make(iri) + .from(iri) .getOrElse(throw new IllegalArgumentException(s"Invalid ProjectIri $iri.")) } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala index a00d2d5133..5d88892f11 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala @@ -21,6 +21,7 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectC import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectSetRestrictedViewSizeRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest import org.knora.webapi.slice.admin.api.service.ProjectADMRestService +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri object ProjectADMRestServiceMock extends Mock[ProjectADMRestService] { object GetProjects extends Effect[Unit, Throwable, ProjectsGetResponseADM] diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala index 4538b3b873..eebc6bbe77 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala @@ -15,11 +15,11 @@ import zio.mock.Proxy import java.util.UUID -import dsp.valueobjects.Iri import org.knora.webapi.messages.admin.responder.projectsmessages.* import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectCreateRequest import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { @@ -31,7 +31,7 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { object ProjectAdminMembersGetRequestADM extends Effect[(ProjectIdentifierADM, UserADM), Throwable, ProjectAdminMembersGetResponseADM] object ProjectsKeywordsGetRequestADM extends Effect[Unit, Throwable, ProjectsKeywordsGetResponseADM] - object ProjectKeywordsGetRequestADM extends Effect[Iri.ProjectIri, Throwable, ProjectKeywordsGetResponseADM] + object ProjectKeywordsGetRequestADM extends Effect[ProjectIri, Throwable, ProjectKeywordsGetResponseADM] object ProjectRestrictedViewSettingsGetADM extends Effect[ProjectIdentifierADM, Throwable, Option[ProjectRestrictedViewSettingsADM]] object ProjectRestrictedViewSettingsGetRequestADM @@ -39,7 +39,7 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { object ProjectCreateRequestADM extends Effect[(ProjectCreateRequest, UserADM, UUID), Throwable, ProjectOperationResponseADM] object ChangeBasicInformationRequestADM - extends Effect[(Iri.ProjectIri, ProjectUpdateRequest, UserADM, UUID), Throwable, ProjectOperationResponseADM] + extends Effect[(ProjectIri, ProjectUpdateRequest, UserADM, UUID), Throwable, ProjectOperationResponseADM] val compose: URLayer[mock.Proxy, ProjectsResponderADM] = ZLayer { @@ -64,7 +64,7 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { proxy(ProjectAdminMembersGetRequestADM, (id, user)) override def projectsKeywordsGetRequestADM(): Task[ProjectsKeywordsGetResponseADM] = proxy(ProjectsKeywordsGetRequestADM, ()) - override def projectKeywordsGetRequestADM(projectIri: Iri.ProjectIri): Task[ProjectKeywordsGetResponseADM] = + override def projectKeywordsGetRequestADM(projectIri: ProjectIri): Task[ProjectKeywordsGetResponseADM] = proxy(ProjectKeywordsGetRequestADM, projectIri) override def projectRestrictedViewSettingsGetADM( id: ProjectIdentifierADM @@ -81,7 +81,7 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { ): Task[ProjectOperationResponseADM] = proxy(ProjectCreateRequestADM, (createReq, requestingUser, apiRequestID)) override def changeBasicInformationRequestADM( - projectIri: Iri.ProjectIri, + projectIri: ProjectIri, updateReq: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala index 97869a305d..b2e775049a 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsServiceLiveSpec.scala @@ -147,7 +147,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { // needs to have the StringFormatter in the environment because the [[ChangeProjectApiRequestADM]] needs it val deleteProjectSpec: Spec[StringFormatter, Throwable] = test("delete a project") { val iri = "http://rdfh.ch/projects/0001" - val projectIri = TestDataFactory.projectIri(iri) + val projectIri = ProjectIri.unsafeFrom(iri) val projectStatus = Some(Status.Inactive) val projectUpdatePayload = ProjectUpdateRequest(status = projectStatus) for { @@ -165,7 +165,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { val updateProjectSpec: Spec[Any, Throwable] = test("update a project") { val iri = "http://rdfh.ch/projects/0001" - val projectIri = TestDataFactory.projectIri(iri) + val projectIri = ProjectIri.unsafeFrom(iri) val projectUpdatePayload = ProjectUpdateRequest( Some(Shortname.unsafeFrom("usn")), Some(Longname.unsafeFrom("updated project longname")), @@ -285,7 +285,7 @@ object ProjectsServiceLiveSpec extends ZIOSpecDefault { val getKeywordsByProjectIri: Spec[Any, Throwable] = test("get keywords of a single project by project IRI") { val iri = "http://rdfh.ch/projects/0001" - val projectIri = TestDataFactory.projectIri(iri) + val projectIri = ProjectIri.unsafeFrom(iri) val mockResponder = ProjectsResponderADMMock.ProjectKeywordsGetRequestADM( assertion = Assertion.equalTo(projectIri), result = Expectation.value(ProjectKeywordsGetResponseADM(Seq.empty[String])) diff --git a/webapi/src/test/scala/dsp/valueobjects/KnoraProjectSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala similarity index 78% rename from webapi/src/test/scala/dsp/valueobjects/KnoraProjectSpec.scala rename to webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala index bba38795f9..127fa825cb 100644 --- a/webapi/src/test/scala/dsp/valueobjects/KnoraProjectSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/model/KnoraProjectSpec.scala @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package dsp.valueobjects +package org.knora.webapi.slice.admin.domain.model import zio.Scope import zio.prelude.Validation @@ -12,13 +12,16 @@ import zio.test.* import scala.util.Random import dsp.errors.ValidationException +import dsp.valueobjects.V2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.* /** - * This spec is used to test the [[KnoraProject]] value objects creation. + * This spec is used to test the [[org.knora.webapi.slice.admin.domain.model.KnoraProject]] value objects creation. */ object KnoraProjectSpec extends ZIOSpecDefault { - def spec: Spec[TestEnvironment & Scope, Nothing] = suite("ProjectSpec")( + + def spec: Spec[TestEnvironment & Scope, Nothing] = suite("KnoraProjectSpec")( + projectIriSuite, shortcodeTest, shortnameTest, longnameTest, @@ -29,6 +32,39 @@ object KnoraProjectSpec extends ZIOSpecDefault { projectSelfJoinTest ) + private val projectIriSuite = suite("ProjectIri")( + test("pass an empty value and return an error") { + assertTrue( + ProjectIri.from("") == Validation.fail(ValidationException("Project IRI cannot be empty.")) + ) + }, + test("pass an invalid value and return an error") { + assertTrue( + ProjectIri.from("not an iri") == Validation.fail(ValidationException("Project IRI is invalid.")) + ) + }, + test("pass an invalid IRI containing unsupported UUID version and return an error") { + val projectIriWithUUIDVersion3 = "http://rdfh.ch/projects/tZjZhGSZMeCLA5VeUmwAmg" + assertTrue( + ProjectIri.from(projectIriWithUUIDVersion3) == Validation.fail( + ValidationException("Invalid UUID used to create IRI. Only versions 4 and 5 are supported.") + ) + ) + }, + test("pass a valid project IRI and successfully create value object") { + val validIris = + Gen.fromIterable( + Seq( + "http://rdfh.ch/projects/0001", + "http://rdfh.ch/projects/CwQ8hXF9Qlm1gl2QE6pTpg", + "http://www.knora.org/ontology/knora-admin#SystemProject", + "http://www.knora.org/ontology/knora-admin#DefaultSharedOntologiesProject" + ) + ) + check(validIris)(iri => assertTrue(ProjectIri.unsafeFrom(iri).value == iri)) + } + ) + private val shortcodeTest = suite("Shortcode")( test("pass an empty value and return an error") { assertTrue( diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/repo/KnoraProjectRepoInMemory.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/repo/KnoraProjectRepoInMemory.scala index 1670baf4d6..de96f0777d 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/repo/KnoraProjectRepoInMemory.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/repo/KnoraProjectRepoInMemory.scala @@ -16,12 +16,12 @@ import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentif import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortcodeIdentifier import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM.ShortnameIdentifier import org.knora.webapi.slice.admin.domain.model.KnoraProject +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo import org.knora.webapi.slice.common.repo.AbstractInMemoryCrudRepository -import org.knora.webapi.slice.resourceinfo.domain.InternalIri final case class KnoraProjectRepoInMemory(projects: Ref[List[KnoraProject]]) - extends AbstractInMemoryCrudRepository[KnoraProject, InternalIri](projects, _.id) + extends AbstractInMemoryCrudRepository[KnoraProject, ProjectIri](projects, _.id) with KnoraProjectRepo { override def findById(id: ProjectIdentifierADM): Task[Option[KnoraProject]] = projects.get.map( diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala index c399912f4d..f861329975 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/domain/service/ProjectADMServiceSpec.scala @@ -14,7 +14,6 @@ import dsp.valueobjects.V2.StringLiteralV2 import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectADM import org.knora.webapi.slice.admin.domain.model.KnoraProject import org.knora.webapi.slice.admin.domain.model.KnoraProject.* -import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.slice.resourceinfo.domain.IriTestConstants object ProjectADMServiceSpec extends ZIOSpecDefault { @@ -44,7 +43,7 @@ object ProjectADMServiceSpec extends ZIOSpecDefault { val shortcode = "0002" val shortname = "someOtherProject" val p: KnoraProject = KnoraProject( - id = InternalIri(IriTestConstants.Project.TestProject), + id = ProjectIri.unsafeFrom(IriTestConstants.Project.TestProject), shortname = Shortname.unsafeFrom(shortname), shortcode = Shortcode.unsafeFrom(shortcode), longname = None,