From 44f914455bb360a2904eb07924679e94075e0963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Sat, 2 Dec 2023 16:55:28 +0100 Subject: [PATCH 01/10] Add offset and projectIri inputs --- .../admin/domain/model/KnoraProject.scala | 10 +++++++ .../common/api/KnoraResponseRenderer.scala | 3 --- .../slice/search/api/SearchEndpoints.scala | 26 ++++++++++++++++++- 3 files changed, 35 insertions(+), 4 deletions(-) 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 dc47b1ed89..6a205b447e 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 @@ -5,6 +5,9 @@ package org.knora.webapi.slice.admin.domain.model +import sttp.tapir.Codec +import sttp.tapir.CodecFormat +import sttp.tapir.DecodeResult import sttp.tapir.Schema import zio.NonEmptyChunk import zio.json.* @@ -12,6 +15,7 @@ import zio.prelude.Validation import scala.util.matching.Regex +import dsp.errors.BadRequestException import dsp.errors.ValidationException import dsp.valueobjects.Iri import dsp.valueobjects.Iri.isProjectIri @@ -43,6 +47,12 @@ object KnoraProject { implicit val codec: JsonCodec[ProjectIri] = JsonCodec[String].transformOrFail(ProjectIri.from(_).toEitherWith(e => e.head.getMessage), _.value) + implicit val tapirCodec: Codec[String, ProjectIri, CodecFormat.TextPlain] = Codec.string.mapDecode(str => + ProjectIri + .from(str) + .fold(f => DecodeResult.Error(f.head.getMessage, BadRequestException(f.head.getMessage)), DecodeResult.Value(_)) + )(_.value) + def unsafeFrom(str: String): ProjectIri = from(str).fold(e => throw e.head, identity) def from(str: String): Validation[ValidationException, ProjectIri] = str match { diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/KnoraResponseRenderer.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/KnoraResponseRenderer.scala index 50f1bb84c3..ec4131ecb8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/KnoraResponseRenderer.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/KnoraResponseRenderer.scala @@ -34,9 +34,6 @@ object KnoraResponseRenderer { final case class FormatOptions(rdfFormat: RdfFormat, schema: ApiV2Schema, rendering: Set[Rendering]) { lazy val schemaRendering: SchemaRendering = SchemaRendering(schema, rendering) } - object FormatOptions { - def from(f: RdfFormat, s: SchemaRendering): FormatOptions = FormatOptions(f, s.schema, s.rendering) - } val layer = ZLayer.derive[KnoraResponseRenderer] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index 4a2f767e3f..d63da25791 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -5,18 +5,42 @@ package org.knora.webapi.slice.search.api +import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.RefinedTypeOps +import eu.timepit.refined.numeric.Greater import org.apache.pekko.http.scaladsl.server.Route import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.* +import sttp.tapir.codec.refined.* import zio.Task import zio.ZLayer import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.responders.v2.SearchResponderV2 +import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri +import org.knora.webapi.slice.common.api.ApiV2 +import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.common.api.HandlerMapper +import org.knora.webapi.slice.common.api.KnoraResponseRenderer import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse -import org.knora.webapi.slice.common.api.* +import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler +import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter + +object SearchEndpointsInputs { + + type Offset = Int Refined Greater[-1] + + object Offset extends RefinedTypeOps[Offset, Int] { + val default: Offset = unsafeFrom(0) + } + + val offset: EndpointInput.Query[Offset] = + query[Offset]("offset").description("The offset to be used for paging.").default(Offset.default) + val limitToProject: EndpointInput.Query[ProjectIri] = + query[ProjectIri]("limitToProject").description("The project to limit the search to.") +} final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { From eb07bb1195536e5f8a319774da82816d3ecbb2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Sun, 3 Dec 2023 10:09:31 +0100 Subject: [PATCH 02/10] Add limit to *Iri inputs --- .../src/main/scala/dsp/valueobjects/Iri.scala | 16 ++++-- .../slice/search/api/SearchEndpoints.scala | 56 ++++++++++++------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/webapi/src/main/scala/dsp/valueobjects/Iri.scala b/webapi/src/main/scala/dsp/valueobjects/Iri.scala index f0be8970ad..a308910ccc 100644 --- a/webapi/src/main/scala/dsp/valueobjects/Iri.scala +++ b/webapi/src/main/scala/dsp/valueobjects/Iri.scala @@ -6,17 +6,14 @@ package dsp.valueobjects import com.google.gwt.safehtml.shared.UriUtils.encodeAllowEscapes +import dsp.errors.{BadRequestException, ValidationException} import org.apache.commons.lang3.StringUtils import org.apache.commons.validator.routines.UrlValidator -import zio.json.JsonDecoder -import zio.json.JsonEncoder +import zio.json.{JsonDecoder, JsonEncoder} import zio.prelude.Validation import scala.util.Try -import dsp.errors.BadRequestException -import dsp.errors.ValidationException - trait Iri { val value: String } @@ -175,6 +172,15 @@ object Iri { if (isUserIri(iri)) toSparqlEncodedString(iri) else None + /** + */ + final case class SimpleIri private (value: String) extends Iri + object SimpleIri { + def from(value: String): Either[String, Iri] = + if (isIri(value)) Right(SimpleIri(value)) + else Left(s"Invalid IRI: $value") + } + /** * GroupIri value object. */ diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index d63da25791..c70c8074bf 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -5,28 +5,26 @@ package org.knora.webapi.slice.search.api -import eu.timepit.refined.api.Refined -import eu.timepit.refined.api.RefinedTypeOps +import dsp.valueobjects.Iri +import eu.timepit.refined.api.{Refined, RefinedTypeOps} import eu.timepit.refined.numeric.Greater import org.apache.pekko.http.scaladsl.server.Route -import sttp.model.HeaderNames -import sttp.model.MediaType -import sttp.tapir.* -import sttp.tapir.codec.refined.* -import zio.Task -import zio.ZLayer - import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.common.api.ApiV2 -import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapper -import org.knora.webapi.slice.common.api.KnoraResponseRenderer -import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions -import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse -import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.{FormatOptions, RenderedResponse} +import org.knora.webapi.slice.common.api.{ + ApiV2, + BaseEndpoints, + HandlerMapper, + KnoraResponseRenderer, + SecuredEndpointAndZioHandler, + TapirToPekkoInterpreter +} +import sttp.model.{HeaderNames, MediaType} +import sttp.tapir.* +import sttp.tapir.codec.refined.* +import zio.{Task, ZLayer} object SearchEndpointsInputs { @@ -36,10 +34,30 @@ object SearchEndpointsInputs { val default: Offset = unsafeFrom(0) } + final case class SimpleIri private (value: String) extends AnyVal + object SimpleIri { + + implicit val tapirCodec: Codec[String, SimpleIri, CodecFormat.TextPlain] = + Codec.string.mapEither(SimpleIri.from)(_.value) + + def from(value: String): Either[String, SimpleIri] = + if (Iri.isIri(value)) { Right(SimpleIri(value)) } + else { Left(s"Invalid IRI: $value") } + + def unsafeFrom(value: String): SimpleIri = from(value).fold(e => throw new IllegalArgumentException(e), identity) + } + val offset: EndpointInput.Query[Offset] = query[Offset]("offset").description("The offset to be used for paging.").default(Offset.default) - val limitToProject: EndpointInput.Query[ProjectIri] = - query[ProjectIri]("limitToProject").description("The project to limit the search to.") + + val limitToProject: EndpointInput.Query[Option[ProjectIri]] = + query[Option[ProjectIri]]("limitToProject").description("The project to limit the search to.") + + val limitToResourceClass: EndpointInput.Query[Option[SimpleIri]] = + query[Option[SimpleIri]]("limitToResourceClass").description("The resource class to limit the search to.") + + val limitToStandoffClass: EndpointInput.Query[Option[SimpleIri]] = + query[Option[SimpleIri]]("limitToStandoffClass").description("The standoff class to limit the search to.") } final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { From eefc906c757c07877b3beab59951107267f206bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Sun, 3 Dec 2023 13:35:02 +0100 Subject: [PATCH 03/10] Migrate search by label endpoints to tapir --- .../src/main/scala/dsp/valueobjects/Iri.scala | 7 +- .../responders/v2/SearchResponderV2.scala | 60 +++++-- .../webapi/routing/v2/SearchRouteV2.scala | 48 +----- .../slice/search/api/SearchEndpoints.scala | 156 ++++++++++++++---- 4 files changed, 174 insertions(+), 97 deletions(-) diff --git a/webapi/src/main/scala/dsp/valueobjects/Iri.scala b/webapi/src/main/scala/dsp/valueobjects/Iri.scala index a308910ccc..00a9bdccf7 100644 --- a/webapi/src/main/scala/dsp/valueobjects/Iri.scala +++ b/webapi/src/main/scala/dsp/valueobjects/Iri.scala @@ -6,14 +6,17 @@ package dsp.valueobjects import com.google.gwt.safehtml.shared.UriUtils.encodeAllowEscapes -import dsp.errors.{BadRequestException, ValidationException} import org.apache.commons.lang3.StringUtils import org.apache.commons.validator.routines.UrlValidator -import zio.json.{JsonDecoder, JsonEncoder} +import zio.json.JsonDecoder +import zio.json.JsonEncoder import zio.prelude.Validation import scala.util.Try +import dsp.errors.BadRequestException +import dsp.errors.ValidationException + trait Iri { val value: String } 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 781ff8980c..b6e55419ac 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 @@ -13,6 +13,7 @@ import dsp.errors.AssertionException import dsp.errors.BadRequestException import dsp.errors.GravsearchException import dsp.errors.InconsistentRepositoryDataException +import dsp.valueobjects.Iri import org.knora.webapi.* import org.knora.webapi.config.AppConfig import org.knora.webapi.core.MessageRelay @@ -168,7 +169,7 @@ trait SearchResponderV2 { * @return a [[ResourceCountV2]] representing the resources that have been found. */ def searchResourcesByLabelCountV2( - searchValue: IRI, + searchValue: String, limitToProject: Option[ProjectIri], limitToResourceClass: Option[SmartIri] ): Task[ResourceCountV2] @@ -185,7 +186,7 @@ trait SearchResponderV2 { * @return a [[ReadResourcesSequenceV2]] representing the resources that have been found. */ def searchResourcesByLabelV2( - searchValue: IRI, + searchValue: String, offset: RuntimeFlags, limitToProject: Option[ProjectIri], limitToResourceClass: Option[SmartIri], @@ -849,14 +850,17 @@ final case class SearchResponderV2Live( } override def searchResourcesByLabelCountV2( - searchValue: IRI, + searchValue: String, limitToProject: Option[ProjectIri], limitToResourceClass: Option[SmartIri] - ): Task[ResourceCountV2] = { - val searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence - val countSparql = - SearchQueries.selectCountByLabel(searchTerm, limitToProject.map(_.value), limitToResourceClass.map(_.toString)) + ): Task[ResourceCountV2] = for { + searchValue <- validateSearchString(searchValue) + _ <- ensureIsNotFullTextSearch(searchValue) + limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri) + searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence + countSparql = + SearchQueries.selectCountByLabel(searchTerm, limitToProject.map(_.value), limitToResourceClass.map(_.toString)) countResponse <- triplestore.query(countSparql) count <- // query response should contain one result with one row with the name "count" @@ -870,6 +874,29 @@ final case class SearchResponderV2Live( .as(countResponse.results.bindings.head.rowMap("count")) } yield ResourceCountV2(count.toInt) + + private def ensureResourceClassIri(resourceClassIri: SmartIri): Task[SmartIri] = { + val errMsg = s"Resource class IRI <$resourceClassIri> is not a valid Knora API v2 entity IRI" + if (resourceClassIri.isKnoraApiV2EntityIri) { + iriConverter.asInternalSmartIri(resourceClassIri).orElseFail(BadRequestException(errMsg)) + } else { ZIO.fail(BadRequestException(errMsg)) } + } + + private def ensureIsNotFullTextSearch(searchStr: String) = + ZIO + .fail(BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route")) + .when(searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) + + private def validateSearchString(searchStr: String) = { + val searchValueMinLength = appConfig.v2.fulltextSearch.searchValueMinLength + ZIO + .fromOption(Iri.toSparqlEncodedString(searchStr)) + .orElseFail(throw BadRequestException(s"Invalid search string: '$searchStr'")) + .filterOrElseWith(_.length >= searchValueMinLength) { it => + val errorMsg = + s"A search value is expected to have at least length of $searchValueMinLength, but '$it' given of length ${it.length}." + ZIO.fail(BadRequestException(errorMsg)) + } } override def searchResourcesByLabelV2( @@ -882,15 +909,18 @@ final case class SearchResponderV2Live( ): Task[ReadResourcesSequenceV2] = { val searchLimit = appConfig.v2.resourcesSequence.resultsPerPage val searchOffset = offset * appConfig.v2.resourcesSequence.resultsPerPage - val searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence - val searchResourceByLabelSparql = SearchQueries.constructSearchByLabel( - searchTerm, - limitToResourceClass.map(_.toIri), - limitToProject.map(_.value), - searchLimit, - searchOffset - ) for { + searchValue <- validateSearchString(searchValue) + _ <- ensureIsNotFullTextSearch(searchValue) + limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri) + searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence + searchResourceByLabelSparql = SearchQueries.constructSearchByLabel( + searchTerm, + limitToResourceClass.map(_.toIri), + limitToProject.map(_.value), + searchLimit, + searchOffset + ) searchResourceByLabelResponse <- triplestore.query(searchResourceByLabelSparql).flatMap(_.asExtended) // collect the IRIs of main resources returned 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 b493905395..c037380cab 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 @@ -36,11 +36,7 @@ final case class SearchRouteV2(searchValueMinLength: Int)( private val LIMIT_TO_STANDOFF_CLASS = "limitToStandoffClass" private val RETURN_FILES = "returnFiles" - def makeRoute: Route = - fullTextSearchCount() ~ - fullTextSearch() ~ - searchByLabelCount() ~ - searchByLabel() + def makeRoute: Route = fullTextSearchCount() ~ fullTextSearch() /** * Gets the requested offset. Returns zero if no offset is indicated. @@ -183,46 +179,4 @@ final case class SearchRouteV2(searchValueMinLength: Int)( RouteUtilV2.completeResponse(requestTask, requestContext, targetSchemaTask, schemaOptionsTask.map(Some(_))) } } - - private def searchByLabelCount(): Route = - path("v2" / "searchbylabel" / "count" / Segment) { - 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 response = for { - searchString <- validateSearchString(searchval) - limitToProject <- getProjectIri(params) - limitToResourceClass <- getResourceClassFromParams(params) - response <- - SearchResponderV2.searchResourcesByLabelCountV2(searchString, limitToProject, limitToResourceClass) - } yield response - RouteUtilV2.completeResponse(response, requestContext, RouteUtilV2.getOntologySchema(requestContext)) - } - } - - private def searchByLabel(): Route = path( - "v2" / "searchbylabel" / Segment - ) { searchval => - get { requestContext => - val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) - val params: Map[String, String] = requestContext.request.uri.query().toMap - val response = for { - sparqlEncodedSearchString <- validateSearchString(searchval) - offset <- getOffsetFromParams(params) - limitToProject <- getProjectIri(params) - limitToResourceClass <- getResourceClassFromParams(params) - targetSchema <- targetSchemaTask - requestingUser <- Authenticator.getUserADM(requestContext) - 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/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index c70c8074bf..d7e6590333 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -5,26 +5,33 @@ package org.knora.webapi.slice.search.api -import dsp.valueobjects.Iri -import eu.timepit.refined.api.{Refined, RefinedTypeOps} +import eu.timepit.refined.api.Refined +import eu.timepit.refined.api.RefinedTypeOps import eu.timepit.refined.numeric.Greater import org.apache.pekko.http.scaladsl.server.Route +import sttp.model.HeaderNames +import sttp.model.MediaType +import sttp.tapir.* +import sttp.tapir.codec.refined.* +import zio.Task +import zio.ZIO +import zio.ZLayer + +import dsp.valueobjects.Iri import org.knora.webapi.messages.admin.responder.usersmessages.UserADM import org.knora.webapi.responders.v2.SearchResponderV2 import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.common.api.KnoraResponseRenderer.{FormatOptions, RenderedResponse} -import org.knora.webapi.slice.common.api.{ - ApiV2, - BaseEndpoints, - HandlerMapper, - KnoraResponseRenderer, - SecuredEndpointAndZioHandler, - TapirToPekkoInterpreter -} -import sttp.model.{HeaderNames, MediaType} -import sttp.tapir.* -import sttp.tapir.codec.refined.* -import zio.{Task, ZLayer} +import org.knora.webapi.slice.common.api.ApiV2 +import org.knora.webapi.slice.common.api.BaseEndpoints +import org.knora.webapi.slice.common.api.HandlerMapper +import org.knora.webapi.slice.common.api.KnoraResponseRenderer +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions +import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse +import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler +import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter +import org.knora.webapi.slice.resourceinfo.domain.IriConverter +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.SimpleIri object SearchEndpointsInputs { @@ -101,6 +108,27 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .out(header[MediaType](HeaderNames.ContentType)) .tags(tags) .description("Count resources using a Gravsearch query.") + + val getSearchByLabel = baseEndpoints.withUserEndpoint.get + .in("v2" / "searchbylabel" / path[String]("searchTerm")) + .in(ApiV2.Inputs.formatOptions) + .in(SearchEndpointsInputs.offset) + .in(SearchEndpointsInputs.limitToProject) + .in(SearchEndpointsInputs.limitToResourceClass) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + .tags(tags) + .description("Search for resources by label.") + + val getSearchByLabelCount = baseEndpoints.withUserEndpoint.get + .in("v2" / "searchbylabel" / "count" / path[String]("searchTerm")) + .in(ApiV2.Inputs.formatOptions) + .in(SearchEndpointsInputs.limitToProject) + .in(SearchEndpointsInputs.limitToResourceClass) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + .tags(tags) + .description("Search for resources by label.") } object SearchEndpoints { @@ -109,49 +137,111 @@ object SearchEndpoints { final case class SearchApiRoutes( searchEndpoints: SearchEndpoints, - searchResponderV2: SearchResponderV2, - renderer: KnoraResponseRenderer, + searchRestService: SearchRestService, mapper: HandlerMapper, - tapirToPekko: TapirToPekkoInterpreter + tapirToPekko: TapirToPekkoInterpreter, + iriConverter: IriConverter ) { private type GravsearchQuery = String - private val gravsearchHandler: UserADM => ((GravsearchQuery, FormatOptions)) => Task[(RenderedResponse, MediaType)] = - u => { case (q, o) => searchResponderV2.gravsearchV2(q, o.schemaRendering, u).flatMap(renderer.render(_, o)) } - private val postGravsearch = SecuredEndpointAndZioHandler[(GravsearchQuery, FormatOptions), (RenderedResponse, MediaType)]( searchEndpoints.postGravsearch, - gravsearchHandler + user => { case (query, opts) => searchRestService.gravsearch(query, opts, user) } ) private val getGravsearch = SecuredEndpointAndZioHandler[(GravsearchQuery, FormatOptions), (RenderedResponse, MediaType)]( searchEndpoints.getGravsearch, - gravsearchHandler + user => { case (query, opts) => searchRestService.gravsearch(query, opts, user) } ) - private val gravsearchCountHandler - : UserADM => ((GravsearchQuery, FormatOptions)) => Task[(RenderedResponse, MediaType)] = - u => { case (q, s) => searchResponderV2.gravsearchCountV2(q, u).flatMap(renderer.render(_, s)) } - private val postGravsearchCount = SecuredEndpointAndZioHandler[(GravsearchQuery, FormatOptions), (RenderedResponse, MediaType)]( searchEndpoints.postGravsearchCount, - gravsearchCountHandler + user => { case (query, opts) => searchRestService.gravsearchCount(query, opts, user) } ) private val getGravsearchCount = SecuredEndpointAndZioHandler[(GravsearchQuery, FormatOptions), (RenderedResponse, MediaType)]( searchEndpoints.getGravsearchCount, - gravsearchCountHandler + user => { case (query, opts) => searchRestService.gravsearchCount(query, opts, user) } + ) + + private val getSearchByLabel = + SecuredEndpointAndZioHandler[ + (String, FormatOptions, Offset, Option[ProjectIri], Option[SimpleIri]), + (RenderedResponse, MediaType) + ]( + searchEndpoints.getSearchByLabel, + user => { case (query, opts, offset, project, resourceClass) => + searchRestService.searchResourcesByLabelV2(query, opts, offset, project, resourceClass, user) + } + ) + + private val getSearchByLabelCount = + SecuredEndpointAndZioHandler[ + (String, FormatOptions, Option[ProjectIri], Option[SimpleIri]), + (RenderedResponse, MediaType) + ]( + searchEndpoints.getSearchByLabelCount, + _ => { case (query, opts, project, resourceClass) => + searchRestService.searchResourcesByLabelCountV2(query, opts, project, resourceClass) + } ) - val routes: Seq[Route] = Seq(postGravsearch, getGravsearch, postGravsearchCount, getGravsearchCount) - .map(it => mapper.mapEndpointAndHandler(it)) - .map(it => tapirToPekko.toRoute(it)) + val routes: Seq[Route] = + Seq(getSearchByLabel, getSearchByLabelCount, postGravsearch, getGravsearch, postGravsearchCount, getGravsearchCount) + .map(it => mapper.mapEndpointAndHandler(it)) + .map(it => tapirToPekko.toRoute(it)) } object SearchApiRoutes { - val layer = SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] + val layer = SearchRestService.layer >+> SearchEndpoints.layer >>> ZLayer.derive[SearchApiRoutes] +} + +final case class SearchRestService( + searchResponderV2: SearchResponderV2, + renderer: KnoraResponseRenderer, + iriConverter: IriConverter +) { + + def searchResourcesByLabelV2( + query: String, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + limitByResourceClass: Option[SimpleIri], + user: UserADM + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- + searchResponderV2.searchResourcesByLabelV2(query, offset.value, project, resourceClass, opts.schema, user) + response <- renderer.render(searchResult, opts) + } yield response + + def searchResourcesByLabelCountV2( + query: String, + opts: FormatOptions, + project: Option[ProjectIri], + limitByResourceClass: Option[SimpleIri] + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- + searchResponderV2.searchResourcesByLabelCountV2(query, project, resourceClass) + response <- renderer.render(searchResult, opts) + } yield response + + def gravsearch(query: String, opts: FormatOptions, user: UserADM): Task[(RenderedResponse, MediaType)] = for { + searchResult <- searchResponderV2.gravsearchV2(query, opts.schemaRendering, user) + response <- renderer.render(searchResult, opts) + } yield response + + def gravsearchCount(query: String, opts: FormatOptions, user: UserADM): Task[(RenderedResponse, MediaType)] = for { + searchResult <- searchResponderV2.gravsearchCountV2(query, user) + response <- renderer.render(searchResult, opts) + } yield response +} +object SearchRestService { + val layer = ZLayer.derive[SearchRestService] } From 70987b5bbedfd258c0a2d136310201ded6b16099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Sun, 3 Dec 2023 13:52:17 +0100 Subject: [PATCH 04/10] Migrate the fulltext search to tapir --- .../webapi/e2e/v2/SearchRouteV2R2RSpec.scala | 7 +- .../responders/v2/SearchResponderV2.scala | 21 +- .../org/knora/webapi/routing/ApiRoutes.scala | 1 - .../webapi/routing/v2/SearchRouteV2.scala | 182 ------------------ .../admin/domain/model/KnoraProject.scala | 9 +- .../slice/search/api/SearchEndpoints.scala | 101 +++++++++- 6 files changed, 121 insertions(+), 200 deletions(-) delete mode 100644 webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala diff --git a/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala b/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala index 3b27749fe8..9c24de9137 100644 --- a/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala +++ b/integration/src/test/scala/org/knora/webapi/e2e/v2/SearchRouteV2R2RSpec.scala @@ -34,7 +34,6 @@ import org.knora.webapi.messages.util.rdf.JsonLDUtil import org.knora.webapi.messages.util.search.SparqlQueryConstants import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.routing.v2.ResourcesRouteV2 -import org.knora.webapi.routing.v2.SearchRouteV2 import org.knora.webapi.routing.v2.StandoffRouteV2 import org.knora.webapi.routing.v2.ValuesRouteV2 import org.knora.webapi.sharedtestdata.SharedTestDataADM @@ -54,13 +53,9 @@ import pekko.http.scaladsl.model.headers.BasicHttpCredentials */ class SearchRouteV2R2RSpec extends R2RSpec { private implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - private val searchPathNew = UnsafeZioRun + private val searchPath = UnsafeZioRun .runOrThrow(ZIO.serviceWith[SearchApiRoutes](_.routes)) .reduce(_ ~ _) - private val searchPathOld = DSPApiDirectives.handleErrors(appConfig)( - SearchRouteV2(routeData.appConfig.v2.fulltextSearch.searchValueMinLength).makeRoute - ) - private val searchPath = searchPathNew ~ searchPathOld private val resourcePath = DSPApiDirectives.handleErrors(appConfig)(ResourcesRouteV2(appConfig).makeRoute) 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 b6e55419ac..5f1960cb6a 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 @@ -251,6 +251,10 @@ final case class SearchResponderV2Live( limitToStandoffClass: Option[SmartIri] ): Task[ResourceCountV2] = for { + _ <- ensureIsFulltextSearch(searchValue) + searchValue <- validateSearchString(searchValue) + limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri) + limitToStandoffClass <- ZIO.foreach(limitToStandoffClass)(ensureStandoffClass) countSparql <- ZIO.attempt( sparql.v2.txt .searchFulltext( @@ -300,6 +304,10 @@ final case class SearchResponderV2Live( ): Task[ReadResourcesSequenceV2] = { import org.knora.webapi.messages.util.search.FullTextMainQueryGenerator.FullTextSearchConstants for { + _ <- ensureIsFulltextSearch(searchValue) + searchValue <- validateSearchString(searchValue) + limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri) + limitToStandoffClass <- ZIO.foreach(limitToStandoffClass)(ensureStandoffClass) searchSparql <- ZIO.attempt( sparql.v2.txt @@ -856,7 +864,7 @@ final case class SearchResponderV2Live( ): Task[ResourceCountV2] = for { searchValue <- validateSearchString(searchValue) - _ <- ensureIsNotFullTextSearch(searchValue) + _ <- ensureIsFulltextSearch(searchValue) limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri) searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence countSparql = @@ -882,7 +890,14 @@ final case class SearchResponderV2Live( } else { ZIO.fail(BadRequestException(errMsg)) } } - private def ensureIsNotFullTextSearch(searchStr: String) = + private def ensureStandoffClass(standoffClassIri: SmartIri): Task[SmartIri] = { + val errMsg = s"Invalid standoff class IRI: $standoffClassIri" + if (standoffClassIri.isApiV2ComplexSchema) { + iriConverter.asInternalSmartIri(standoffClassIri).orElseFail(BadRequestException(errMsg)) + } else { ZIO.fail(BadRequestException(errMsg)) } + } + + private def ensureIsFulltextSearch(searchStr: String) = ZIO .fail(BadRequestException("It looks like you are submitting a Gravsearch request to a full-text search route")) .when(searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) @@ -911,7 +926,7 @@ final case class SearchResponderV2Live( val searchOffset = offset * appConfig.v2.resourcesSequence.resultsPerPage for { searchValue <- validateSearchString(searchValue) - _ <- ensureIsNotFullTextSearch(searchValue) + _ <- ensureIsFulltextSearch(searchValue) limitToResourceClass <- ZIO.foreach(limitToResourceClass)(ensureResourceClassIri) searchTerm = MatchStringWhileTyping(searchValue).generateLiteralForLuceneIndexWithoutExactSequence searchResourceByLabelSparql = SearchQueries.constructSearchByLabel( 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 da34276929..c4835b3a3b 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -109,7 +109,6 @@ private final case class ApiRoutesImpl( PermissionsRouteADM(routeData, runtime).makeRoute ~ RejectingRoute(appConfig, runtime).makeRoute ~ ResourcesRouteV2(appConfig).makeRoute ~ - SearchRouteV2(appConfig.v2.fulltextSearch.searchValueMinLength).makeRoute ~ StandoffRouteV2().makeRoute ~ StoreRouteADM(routeData, runtime).makeRoute ~ UsersRouteADM().makeRoute ~ 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 deleted file mode 100644 index c037380cab..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/routing/v2/SearchRouteV2.scala +++ /dev/null @@ -1,182 +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.routing.v2 - -import org.apache.pekko.http.scaladsl.server.Directives.* -import org.apache.pekko.http.scaladsl.server.Route -import zio.* - -import dsp.errors.BadRequestException -import dsp.valueobjects.Iri -import org.knora.webapi.* -import org.knora.webapi.config.AppConfig -import org.knora.webapi.core.MessageRelay -import org.knora.webapi.messages.OntologyConstants -import org.knora.webapi.messages.SmartIri -import org.knora.webapi.messages.ValuesValidator -import org.knora.webapi.responders.v2.SearchResponderV2 -import org.knora.webapi.routing.Authenticator -import org.knora.webapi.routing.RouteUtilV2 -import org.knora.webapi.slice.admin.domain.model.KnoraProject.ProjectIri -import org.knora.webapi.slice.resourceinfo.domain.IriConverter - -/** - * Provides a function for API routes that deal with search. - */ -final case class SearchRouteV2(searchValueMinLength: Int)( - private implicit val runtime: Runtime[AppConfig & Authenticator & IriConverter & SearchResponderV2 & MessageRelay] -) { - - private val LIMIT_TO_PROJECT = "limitToProject" - private val LIMIT_TO_RESOURCE_CLASS = "limitToResourceClass" - private val OFFSET = "offset" - private val LIMIT_TO_STANDOFF_CLASS = "limitToStandoffClass" - private val RETURN_FILES = "returnFiles" - - def makeRoute: Route = fullTextSearchCount() ~ fullTextSearch() - - /** - * Gets the requested offset. Returns zero if no offset is indicated. - * - * @param params the GET parameters. - * @return the offset to be used for paging. - */ - private def getOffsetFromParams(params: Map[String, String]): IO[BadRequestException, Int] = - params - .get(OFFSET) - .map { offsetStr => - ZIO - .fromOption(ValuesValidator.validateInt(offsetStr)) - .orElseFail(BadRequestException(s"offset is expected to be an Integer, but $offsetStr given")) - .filterOrFail(_ >= 0)( - BadRequestException(s"offset is expected to be a positive Integer, but $offsetStr given") - ) - } - .getOrElse(ZIO.succeed(0)) - - private def getProjectIri(params: Map[String, String]): IO[BadRequestException, Option[ProjectIri]] = - ZIO - .fromOption(params.get(LIMIT_TO_PROJECT)) - .flatMap(iri => ProjectIri.from(iri).toZIO.orElseFail(Some(BadRequestException(s"$iri is not a valid IRI.")))) - .unsome - - /** - * Gets the resource class the search should be restricted to, if any. - * - * @param params the GET parameters. - * @return the internal resource class, if any. - */ - private def getResourceClassFromParams( - params: Map[String, String] - ): ZIO[IriConverter, BadRequestException, Option[SmartIri]] = - params - .get(LIMIT_TO_RESOURCE_CLASS) - .map { resourceClassIriStr => - IriConverter - .asSmartIri(resourceClassIriStr) - .orElseFail(BadRequestException(s"Invalid resource class IRI: $resourceClassIriStr")) - .filterOrFail(_.isKnoraApiV2EntityIri)( - BadRequestException(s"$resourceClassIriStr is not a valid knora-api resource class IRI") - ) - .map(asSomeIriWithInternalSchema) - } - .getOrElse(ZIO.none) - - private def asSomeIriWithInternalSchema(iri: SmartIri): Some[SmartIri] = Some(iri.toOntologySchema(InternalSchema)) - - /** - * Gets the standoff class the search should be restricted to. - * - * @param params the GET parameters. - * @return the internal standoff class, if any. - */ - private def getStandoffClass(params: Map[String, String]): ZIO[IriConverter, BadRequestException, Option[SmartIri]] = - params - .get(LIMIT_TO_STANDOFF_CLASS) - .map { standoffClassIriStr => - IriConverter - .asSmartIri(standoffClassIriStr) - .orElseFail(BadRequestException(s"Invalid standoff class IRI: $standoffClassIriStr")) - .filterOrFail(_.isApiV2ComplexSchema)( - BadRequestException(s"$standoffClassIriStr is not a valid knora-api standoff class IRI") - ) - .map(asSomeIriWithInternalSchema) - } - .getOrElse(ZIO.none) - - private def fullTextSearchCount(): Route = - path("v2" / "search" / "count" / Segment) { - 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 response = for { - _ <- ensureIsNotFullTextSearch(searchStr) - escapedSearchStr <- validateSearchString(searchStr) - limitToProject <- getProjectIri(params) - limitToResourceClass <- getResourceClassFromParams(params) - limitToStandoffClass <- getStandoffClass(params) - response <- SearchResponderV2.fulltextSearchCountV2( - escapedSearchStr, - limitToProject, - limitToResourceClass, - limitToStandoffClass - ) - } yield response - RouteUtilV2.completeResponse(response, requestContext) - } - } - - private def validateSearchString(searchStr: String) = - ZIO - .fromOption(Iri.toSparqlEncodedString(searchStr)) - .orElseFail(throw BadRequestException(s"Invalid search string: '$searchStr'")) - .filterOrElseWith(_.length >= searchValueMinLength) { it => - val errorMsg = - s"A search value is expected to have at least length of $searchValueMinLength, but '$it' given of length ${it.length}." - ZIO.fail(BadRequestException(errorMsg)) - } - - private def ensureIsNotFullTextSearch(searchStr: String) = - ZIO - .fail( - BadRequestException( - "It looks like you are submitting a Gravsearch request to a full-text search route" - ) - ) - .when(searchStr.contains(OntologyConstants.KnoraApi.ApiOntologyHostname)) - - private def fullTextSearch(): Route = path("v2" / "search" / Segment) { - searchStr => // TODO: if a space is encoded as a "+", this is not converted back to a space - get { requestContext => - val targetSchemaTask = RouteUtilV2.getOntologySchema(requestContext) - val schemaOptionsTask = RouteUtilV2.getSchemaOptions(requestContext) - - val params: Map[String, String] = requestContext.request.uri.query().toMap - val requestTask = for { - _ <- ensureIsNotFullTextSearch(searchStr) - escapedSearchStr <- validateSearchString(searchStr) - offset <- getOffsetFromParams(params) - limitToProject <- getProjectIri(params) - limitToResourceClass <- getResourceClassFromParams(params) - limitToStandoffClass <- getStandoffClass(params) - returnFiles = ValuesValidator.optionStringToBoolean(params.get(RETURN_FILES), fallback = false) - requestingUser <- Authenticator.getUserADM(requestContext) - schemaAndOptions <- targetSchemaTask.zip(schemaOptionsTask).map { case (s, o) => SchemaRendering(s, o) } - response <- SearchResponderV2.fulltextSearchV2( - escapedSearchStr, - offset, - limitToProject, - limitToResourceClass, - limitToStandoffClass, - returnFiles, - schemaAndOptions, - requestingUser - ) - } yield response - RouteUtilV2.completeResponse(requestTask, requestContext, targetSchemaTask, schemaOptionsTask.map(Some(_))) - } - } -} 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 6a205b447e..c0f3e62023 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 @@ -7,7 +7,6 @@ package org.knora.webapi.slice.admin.domain.model import sttp.tapir.Codec import sttp.tapir.CodecFormat -import sttp.tapir.DecodeResult import sttp.tapir.Schema import zio.NonEmptyChunk import zio.json.* @@ -15,7 +14,6 @@ import zio.prelude.Validation import scala.util.matching.Regex -import dsp.errors.BadRequestException import dsp.errors.ValidationException import dsp.valueobjects.Iri import dsp.valueobjects.Iri.isProjectIri @@ -47,11 +45,8 @@ object KnoraProject { implicit val codec: JsonCodec[ProjectIri] = JsonCodec[String].transformOrFail(ProjectIri.from(_).toEitherWith(e => e.head.getMessage), _.value) - implicit val tapirCodec: Codec[String, ProjectIri, CodecFormat.TextPlain] = Codec.string.mapDecode(str => - ProjectIri - .from(str) - .fold(f => DecodeResult.Error(f.head.getMessage, BadRequestException(f.head.getMessage)), DecodeResult.Value(_)) - )(_.value) + implicit val tapirCodec: Codec[String, ProjectIri, CodecFormat.TextPlain] = + Codec.string.mapEither(str => ProjectIri.from(str).toEitherWith(e => e.head.getMessage))(_.value) def unsafeFrom(str: String): ProjectIri = from(str).fold(e => throw e.head, identity) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index d7e6590333..928d18035a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -65,6 +65,9 @@ object SearchEndpointsInputs { val limitToStandoffClass: EndpointInput.Query[Option[SimpleIri]] = query[Option[SimpleIri]]("limitToStandoffClass").description("The standoff class to limit the search to.") + + val returnFiles: EndpointInput.Query[Boolean] = + query[Boolean]("returnFiles").description("Whether to return files in the search results.").default(false) } final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { @@ -129,6 +132,30 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .out(header[MediaType](HeaderNames.ContentType)) .tags(tags) .description("Search for resources by label.") + + val getFullTextSearch = baseEndpoints.withUserEndpoint.get + .in("v2" / "search" / path[String]("searchTerm")) + .in(ApiV2.Inputs.formatOptions) + .in(SearchEndpointsInputs.offset) + .in(SearchEndpointsInputs.limitToProject) + .in(SearchEndpointsInputs.limitToResourceClass) + .in(SearchEndpointsInputs.limitToStandoffClass) + .in(SearchEndpointsInputs.returnFiles) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + .tags(tags) + .description("Search for resources by label.") + + val getFullTextSearchCount = baseEndpoints.withUserEndpoint.get + .in("v2" / "search" / "count" / path[String]("searchTerm")) + .in(ApiV2.Inputs.formatOptions) + .in(SearchEndpointsInputs.limitToProject) + .in(SearchEndpointsInputs.limitToResourceClass) + .in(SearchEndpointsInputs.limitToStandoffClass) + .out(stringBody) + .out(header[MediaType](HeaderNames.ContentType)) + .tags(tags) + .description("Search for resources by label.") } object SearchEndpoints { @@ -190,8 +217,39 @@ final case class SearchApiRoutes( } ) + private val getFullTextSearch = + SecuredEndpointAndZioHandler[ + (String, FormatOptions, Offset, Option[ProjectIri], Option[SimpleIri], Option[SimpleIri], Boolean), + (RenderedResponse, MediaType) + ]( + searchEndpoints.getFullTextSearch, + user => { case (query, opts, offset, project, resourceClass, standoffClass, returnFiles) => + searchRestService.fullTextSearch(query, opts, offset, project, resourceClass, standoffClass, returnFiles, user) + } + ) + + private val getFullTextSearchCount = + SecuredEndpointAndZioHandler[ + (String, FormatOptions, Option[ProjectIri], Option[SimpleIri], Option[SimpleIri]), + (RenderedResponse, MediaType) + ]( + searchEndpoints.getFullTextSearchCount, + _ => { case (query, opts, project, resourceClass, standoffClass) => + searchRestService.fullTextSearchCount(query, opts, project, resourceClass, standoffClass) + } + ) + val routes: Seq[Route] = - Seq(getSearchByLabel, getSearchByLabelCount, postGravsearch, getGravsearch, postGravsearchCount, getGravsearchCount) + Seq( + getFullTextSearch, + getFullTextSearchCount, + getSearchByLabel, + getSearchByLabelCount, + postGravsearch, + getGravsearch, + postGravsearchCount, + getGravsearchCount + ) .map(it => mapper.mapEndpointAndHandler(it)) .map(it => tapirToPekko.toRoute(it)) } @@ -241,7 +299,48 @@ final case class SearchRestService( searchResult <- searchResponderV2.gravsearchCountV2(query, user) response <- renderer.render(searchResult, opts) } yield response + + def fullTextSearch( + query: RenderedResponse, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + resourceClass: Option[SimpleIri], + standoffClass: Option[SimpleIri], + returnFiles: Boolean, + user: UserADM + ): Task[(RenderedResponse, MediaType)] = for { + resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) + standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- searchResponderV2.fulltextSearchV2( + query, + offset.value, + project, + resourceClass, + standoffClass, + returnFiles, + opts.schemaRendering, + user + ) + response <- renderer.render(searchResult, opts) + } yield response + + def fullTextSearchCount( + query: RenderedResponse, + opts: FormatOptions, + project: Option[ProjectIri], + resourceClass: Option[SimpleIri], + standoffClass: Option[SimpleIri] + ): zio.Task[ + (_root_.org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse, _root_.sttp.model.MediaType) + ] = for { + resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) + standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) + searchResult <- searchResponderV2.fulltextSearchCountV2(query, project, resourceClass, standoffClass) + response <- renderer.render(searchResult, opts) + } yield response } + object SearchRestService { val layer = ZLayer.derive[SearchRestService] } From 66e58168452690d498340485655e1328582b7bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Sun, 3 Dec 2023 14:41:56 +0100 Subject: [PATCH 05/10] fix SearchResponderV2 test --- .../responders/v2/SearchResponderV2Spec.scala | 109 +++++++++++------- .../responders/v2/SearchResponderV2.scala | 43 +------ 2 files changed, 71 insertions(+), 81 deletions(-) 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 26d660b401..6400b164ba 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 @@ -18,6 +18,7 @@ import org.knora.webapi.responders.v2.ResourcesResponseCheckerV2.compareReadReso import org.knora.webapi.routing.UnsafeZioRun import org.knora.webapi.sharedtestdata.SharedTestDataADM import org.knora.webapi.sharedtestdata.SharedTestDataADM.anonymousUser +import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.util.ZioScalaTestUtil.assertFailsWithA class SearchResponderV2Spec extends CoreSpec { @@ -151,60 +152,84 @@ class SearchResponderV2Spec extends CoreSpec { } "perform a search by label for incunabula:book that contain 'Narrenschiff'" in { - 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 - ) - ) - - assert(result.resources.size == 3) + val actual = UnsafeZioRun.runOrThrow { + for { + limitToResourceClass <- IriConverter + .asSmartIri("http://www.knora.org/ontology/0803/incunabula#book") + .mapAttempt(_.toOntologySchema(ApiV2Complex)) + .map(Some(_)) + result <- SearchResponderV2.searchResourcesByLabelV2( + searchValue = "Narrenschiff", + offset = 0, + limitToProject = None, + limitToResourceClass, + targetSchema = ApiV2Complex, + requestingUser = anonymousUser + ) + } yield result + } + + assert(actual.resources.size == 3) } "perform a search by label for incunabula:book that contain 'Das Narrenschiff'" in { - 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 - ) - ) - - assert(result.resources.size == 3) + val actual = UnsafeZioRun.runOrThrow { + for { + limitToResourceClass <- IriConverter + .asSmartIri("http://www.knora.org/ontology/0803/incunabula#book") + .mapAttempt(_.toOntologySchema(ApiV2Complex)) + .map(Some(_)) + result <- SearchResponderV2.searchResourcesByLabelV2( + searchValue = "Narrenschiff", + offset = 0, + limitToProject = None, + limitToResourceClass, + targetSchema = ApiV2Complex, + requestingUser = anonymousUser + ) + } yield result + } + + assert(actual.resources.size == 3) } "perform a count search query by label for incunabula:book that contain 'Narrenschiff'" in { - val result = UnsafeZioRun.runOrThrow( - SearchResponderV2.searchResourcesByLabelCountV2( - searchValue = "Narrenschiff", - limitToProject = None, - limitToResourceClass = Some("http://www.knora.org/ontology/0803/incunabula#book".toSmartIri) - ) - ) - - assert(result.numberOfResources == 3) + val actual = UnsafeZioRun.runOrThrow { + for { + limitToResourceClass <- IriConverter + .asSmartIri("http://www.knora.org/ontology/0803/incunabula#book") + .mapAttempt(_.toOntologySchema(ApiV2Complex)) + .map(Some(_)) + result <- SearchResponderV2.searchResourcesByLabelCountV2( + searchValue = "Narrenschiff", + limitToProject = None, + limitToResourceClass + ) + } yield result + } + + assert(actual.numberOfResources == 3) } "perform a a count search query by label for incunabula:book that contain 'Passio sancti Meynrhadi martyris et heremite'" in { - 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) - ) - ) - - assert(result.numberOfResources == 1) + val actual = UnsafeZioRun.runOrThrow { + for { + limitToResourceClass <- IriConverter + .asSmartIri("http://www.knora.org/ontology/0803/incunabula#book") + .mapAttempt(_.toOntologySchema(ApiV2Complex)) + .map(Some(_)) + result <- SearchResponderV2.searchResourcesByLabelCountV2( + searchValue = "Passio sancti Meynrhadi martyris et heremite", + limitToProject = None, + limitToResourceClass + ) + } yield result + } + + assert(actual.numberOfResources == 1) } "search by project and resource class" in { 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 5f1960cb6a..c84bc72884 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 @@ -226,12 +226,14 @@ final case class SearchResponderV2Live( private val sparqlTransformerLive: OntologyInferencer, private val gravsearchTypeInspectionRunner: GravsearchTypeInspectionRunner, private val inferenceOptimizationService: InferenceOptimizationService, - implicit private val stringFormatter: StringFormatter, + private val stringFormatter: StringFormatter, private val iriConverter: IriConverter, private val constructTransformer: ConstructTransformer ) extends SearchResponderV2 with LazyLogging { + private implicit val sf: StringFormatter = stringFormatter + /** * Performs a fulltext search and returns the resources count (how many resources match the search criteria), * without taking into consideration permission checking. @@ -1057,42 +1059,5 @@ final case class SearchResponderV2Live( } object SearchResponderV2Live { - val layer: ZLayer[ - AppConfig & TriplestoreService & MessageRelay & ConstructResponseUtilV2 & OntologyCache & StandoffTagUtilV2 & - QueryTraverser & OntologyInferencer & GravsearchTypeInspectionRunner & InferenceOptimizationService & - IriConverter & ConstructTransformer & StringFormatter, - Nothing, - SearchResponderV2Live - ] = - ZLayer.fromZIO( - for { - appConfig <- ZIO.service[AppConfig] - triplestoreService <- ZIO.service[TriplestoreService] - messageRelay <- ZIO.service[MessageRelay] - constructResponseUtilV2 <- ZIO.service[ConstructResponseUtilV2] - ontologyCache <- ZIO.service[OntologyCache] - standoffTagUtilV2 <- ZIO.service[StandoffTagUtilV2] - queryTraverser <- ZIO.service[QueryTraverser] - sparqlTransformerLive <- ZIO.service[OntologyInferencer] - stringFormatter <- ZIO.service[StringFormatter] - typeInspectionRunner <- ZIO.service[GravsearchTypeInspectionRunner] - inferenceOptimizationService <- ZIO.service[InferenceOptimizationService] - iriConverter <- ZIO.service[IriConverter] - constructTransformer <- ZIO.service[ConstructTransformer] - } yield new SearchResponderV2Live( - appConfig, - triplestoreService, - messageRelay, - constructResponseUtilV2, - ontologyCache, - standoffTagUtilV2, - queryTraverser, - sparqlTransformerLive, - typeInspectionRunner, - inferenceOptimizationService, - stringFormatter, - iriConverter, - constructTransformer - ) - ) + val layer = ZLayer.derive[SearchResponderV2Live] } From 1590579f34c42898822dc25864b9e20311973a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 4 Dec 2023 09:58:27 +0100 Subject: [PATCH 06/10] Move defaultRdfFormart constant to RdfFormat.default --- .../knora/webapi/messages/util/rdf/RdfFormatUtil.scala | 6 +++--- .../scala/org/knora/webapi/slice/common/api/ApiV2.scala | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala index b80be9683b..2d44bf09ca 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/util/rdf/RdfFormatUtil.scala @@ -25,7 +25,6 @@ import org.knora.webapi.IRI import org.knora.webapi.RdfMediaTypes import org.knora.webapi.Rendering import org.knora.webapi.SchemaOptions -import org.knora.webapi.slice.common.api.ApiV2 /** * A trait for supported RDF formats. @@ -57,11 +56,12 @@ sealed trait QuadFormat extends NonJsonLD object RdfFormat { + val default: RdfFormat = JsonLD + val values: Seq[RdfFormat] = Seq(JsonLD, Turtle, TriG, RdfXml, NQuads) - def from(mediaType: model.MediaType): RdfFormat = values + def from(mediaType: model.MediaType): Option[RdfFormat] = values .find(_.mediaType.equalsIgnoreParameters(mediaType)) - .getOrElse(ApiV2.Inputs.defaultRdfFormat) /** * Converts a [[MediaType]] to an [[RdfFormat]]. diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala index d6488f5d4e..40d978a11a 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala @@ -22,7 +22,6 @@ import org.knora.webapi.ApiV2Schema import org.knora.webapi.JsonLdRendering import org.knora.webapi.MarkupRendering import org.knora.webapi.Rendering -import org.knora.webapi.messages.util.rdf.JsonLD import org.knora.webapi.messages.util.rdf.RdfFormat import org.knora.webapi.slice.common.api.KnoraResponseRenderer.FormatOptions @@ -121,13 +120,14 @@ object ApiV2 { } // RdfFormat input - val defaultRdfFormat: RdfFormat = JsonLD private val rdfFormat: EndpointIO.Header[RdfFormat] = header[Option[MediaType]](HeaderNames.Accept) .description( s"""The RDF format to be used for the request. Valid values are: ${RdfFormat.values} - |If not specified or unknown, the fallback RDF format $defaultRdfFormat will be used.""".stripMargin + |If not specified or unknown, the fallback RDF format ${RdfFormat.default} will be used.""".stripMargin + ) + .mapDecode(s => DecodeResult.Value(s.flatMap(RdfFormat.from).getOrElse(RdfFormat.default)))(it => + Some(it.mediaType) ) - .mapDecode(s => DecodeResult.Value(s.map(RdfFormat.from).getOrElse(defaultRdfFormat)))(it => Some(it.mediaType)) .validate(Validator.pass[RdfFormat]) // FormatOptions input From 59eb8221c2ad51b218c1ae94d68760dbb9d6c34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 4 Dec 2023 10:04:21 +0100 Subject: [PATCH 07/10] simplify --- .../org/knora/webapi/slice/common/api/ApiV2.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala index 40d978a11a..742ab37cff 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/ApiV2.scala @@ -8,7 +8,6 @@ package org.knora.webapi.slice.common.api import sttp.model.HeaderNames import sttp.model.MediaType import sttp.tapir.Codec -import sttp.tapir.Codec.PlainCodec import sttp.tapir.CodecFormat import sttp.tapir.DecodeResult import sttp.tapir.EndpointIO @@ -17,7 +16,6 @@ import sttp.tapir.Validator import sttp.tapir.header import sttp.tapir.query -import dsp.errors.BadRequestException import org.knora.webapi.ApiV2Schema import org.knora.webapi.JsonLdRendering import org.knora.webapi.MarkupRendering @@ -140,17 +138,14 @@ object ApiV2 { } private object Codecs { - private def codecFromStringCodec[A](f: String => Either[String, A], g: A => String): PlainCodec[A] = - Codec.string.mapDecode(f(_).fold(e => DecodeResult.Error(e, BadRequestException(e)), DecodeResult.Value(_)))(g) - // Codec for ApiV2Schema implicit val apiV2SchemaListCodec: Codec[List[String], Option[ApiV2Schema], CodecFormat.TextPlain] = - Codec.listHeadOption(codecFromStringCodec(ApiV2Schema.from, _.name)) + Codec.listHeadOption(Codec.string.mapEither(ApiV2Schema.from)(_.name)) // Codecs for Rendering (JsonLdRendering and MarkupRendering) implicit val jsonLdRenderingListCodec: Codec[List[String], Option[JsonLdRendering], CodecFormat.TextPlain] = - Codec.listHeadOption(codecFromStringCodec(JsonLdRendering.from, _.name)) + Codec.listHeadOption(Codec.string.mapEither(JsonLdRendering.from)(_.name)) implicit val markupRenderingListCode: Codec[List[String], Option[MarkupRendering], CodecFormat.TextPlain] = - Codec.listHeadOption(codecFromStringCodec(MarkupRendering.from, _.name)) + Codec.listHeadOption(Codec.string.mapEither(MarkupRendering.from)(_.name)) } } From ba4d38e345ec55ef506db8c051e950dbeda94b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 4 Dec 2023 11:31:26 +0100 Subject: [PATCH 08/10] inline --- .../webapi/slice/search/api/SearchEndpoints.scala | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index 928d18035a..368899d7b6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -73,13 +73,11 @@ object SearchEndpointsInputs { final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { private val tags = List("v2", "search") - private val searchBase = "v2" / "searchextended" - private val gravsearchDescription = "The Gravsearch query. See https://docs.dasch.swiss/latest/DSP-API/03-endpoints/api-v2/query-language/" val postGravsearch = baseEndpoints.withUserEndpoint.post - .in(searchBase) + .in("v2" / "searchextended") .in(stringBody.description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .out(stringBody) @@ -88,7 +86,7 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .description("Search for resources using a Gravsearch query.") val getGravsearch = baseEndpoints.withUserEndpoint.get - .in(searchBase / path[String].description(gravsearchDescription)) + .in("v2" / "searchextended" / path[String].description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) @@ -96,7 +94,7 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .description("Search for resources using a Gravsearch query.") val postGravsearchCount = baseEndpoints.withUserEndpoint.post - .in(searchBase / "count") + .in("v2" / "searchextended" / "count") .in(stringBody.description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .out(stringBody) @@ -105,7 +103,7 @@ final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { .description("Count resources using a Gravsearch query.") val getGravsearchCount = baseEndpoints.withUserEndpoint.get - .in(searchBase / "count" / path[String].description(gravsearchDescription)) + .in("v2" / "searchextended" / "count" / path[String].description(gravsearchDescription)) .in(ApiV2.Inputs.formatOptions) .out(stringBody) .out(header[MediaType](HeaderNames.ContentType)) From 80e791474c0ef8ebeae9e916cd904bd7314db1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 4 Dec 2023 13:15:41 +0100 Subject: [PATCH 09/10] Rename InputQuery --- .../slice/search/api/SearchEndpoints.scala | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index 368899d7b6..bdc62f2d33 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -31,7 +31,7 @@ import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.resourceinfo.domain.IriConverter import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset -import org.knora.webapi.slice.search.api.SearchEndpointsInputs.SimpleIri +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri object SearchEndpointsInputs { @@ -41,17 +41,17 @@ object SearchEndpointsInputs { val default: Offset = unsafeFrom(0) } - final case class SimpleIri private (value: String) extends AnyVal - object SimpleIri { + final case class InputIri private(value: String) extends AnyVal + object InputIri { - implicit val tapirCodec: Codec[String, SimpleIri, CodecFormat.TextPlain] = - Codec.string.mapEither(SimpleIri.from)(_.value) + implicit val tapirCodec: Codec[String, InputIri, CodecFormat.TextPlain] = + Codec.string.mapEither(InputIri.from)(_.value) - def from(value: String): Either[String, SimpleIri] = - if (Iri.isIri(value)) { Right(SimpleIri(value)) } + def from(value: String): Either[String, InputIri] = + if (Iri.isIri(value)) { Right(InputIri(value)) } else { Left(s"Invalid IRI: $value") } - def unsafeFrom(value: String): SimpleIri = from(value).fold(e => throw new IllegalArgumentException(e), identity) + def unsafeFrom(value: String): InputIri = from(value).fold(e => throw new IllegalArgumentException(e), identity) } val offset: EndpointInput.Query[Offset] = @@ -60,11 +60,11 @@ object SearchEndpointsInputs { val limitToProject: EndpointInput.Query[Option[ProjectIri]] = query[Option[ProjectIri]]("limitToProject").description("The project to limit the search to.") - val limitToResourceClass: EndpointInput.Query[Option[SimpleIri]] = - query[Option[SimpleIri]]("limitToResourceClass").description("The resource class to limit the search to.") + val limitToResourceClass: EndpointInput.Query[Option[InputIri]] = + query[Option[InputIri]]("limitToResourceClass").description("The resource class to limit the search to.") - val limitToStandoffClass: EndpointInput.Query[Option[SimpleIri]] = - query[Option[SimpleIri]]("limitToStandoffClass").description("The standoff class to limit the search to.") + val limitToStandoffClass: EndpointInput.Query[Option[InputIri]] = + query[Option[InputIri]]("limitToStandoffClass").description("The standoff class to limit the search to.") val returnFiles: EndpointInput.Query[Boolean] = query[Boolean]("returnFiles").description("Whether to return files in the search results.").default(false) @@ -195,7 +195,7 @@ final case class SearchApiRoutes( private val getSearchByLabel = SecuredEndpointAndZioHandler[ - (String, FormatOptions, Offset, Option[ProjectIri], Option[SimpleIri]), + (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri]), (RenderedResponse, MediaType) ]( searchEndpoints.getSearchByLabel, @@ -206,7 +206,7 @@ final case class SearchApiRoutes( private val getSearchByLabelCount = SecuredEndpointAndZioHandler[ - (String, FormatOptions, Option[ProjectIri], Option[SimpleIri]), + (String, FormatOptions, Option[ProjectIri], Option[InputIri]), (RenderedResponse, MediaType) ]( searchEndpoints.getSearchByLabelCount, @@ -217,7 +217,7 @@ final case class SearchApiRoutes( private val getFullTextSearch = SecuredEndpointAndZioHandler[ - (String, FormatOptions, Offset, Option[ProjectIri], Option[SimpleIri], Option[SimpleIri], Boolean), + (String, FormatOptions, Offset, Option[ProjectIri], Option[InputIri], Option[InputIri], Boolean), (RenderedResponse, MediaType) ]( searchEndpoints.getFullTextSearch, @@ -228,7 +228,7 @@ final case class SearchApiRoutes( private val getFullTextSearchCount = SecuredEndpointAndZioHandler[ - (String, FormatOptions, Option[ProjectIri], Option[SimpleIri], Option[SimpleIri]), + (String, FormatOptions, Option[ProjectIri], Option[InputIri], Option[InputIri]), (RenderedResponse, MediaType) ]( searchEndpoints.getFullTextSearchCount, @@ -263,12 +263,12 @@ final case class SearchRestService( ) { def searchResourcesByLabelV2( - query: String, - opts: FormatOptions, - offset: Offset, - project: Option[ProjectIri], - limitByResourceClass: Option[SimpleIri], - user: UserADM + query: String, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + limitByResourceClass: Option[InputIri], + user: UserADM ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) searchResult <- @@ -280,7 +280,7 @@ final case class SearchRestService( query: String, opts: FormatOptions, project: Option[ProjectIri], - limitByResourceClass: Option[SimpleIri] + limitByResourceClass: Option[InputIri] ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) searchResult <- @@ -299,14 +299,14 @@ final case class SearchRestService( } yield response def fullTextSearch( - query: RenderedResponse, - opts: FormatOptions, - offset: Offset, - project: Option[ProjectIri], - resourceClass: Option[SimpleIri], - standoffClass: Option[SimpleIri], - returnFiles: Boolean, - user: UserADM + query: RenderedResponse, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + resourceClass: Option[InputIri], + standoffClass: Option[InputIri], + returnFiles: Boolean, + user: UserADM ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) @@ -324,11 +324,11 @@ final case class SearchRestService( } yield response def fullTextSearchCount( - query: RenderedResponse, - opts: FormatOptions, - project: Option[ProjectIri], - resourceClass: Option[SimpleIri], - standoffClass: Option[SimpleIri] + query: RenderedResponse, + opts: FormatOptions, + project: Option[ProjectIri], + resourceClass: Option[InputIri], + standoffClass: Option[InputIri] ): zio.Task[ (_root_.org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse, _root_.sttp.model.MediaType) ] = for { From b697ed2973cc9bbf47375a6fd5053824ee573502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Tue, 5 Dec 2023 10:17:56 +0100 Subject: [PATCH 10/10] fmt --- .../slice/search/api/SearchEndpoints.scala | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala index bdc62f2d33..721541bdd7 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/search/api/SearchEndpoints.scala @@ -30,8 +30,8 @@ import org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.resourceinfo.domain.IriConverter -import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset import org.knora.webapi.slice.search.api.SearchEndpointsInputs.InputIri +import org.knora.webapi.slice.search.api.SearchEndpointsInputs.Offset object SearchEndpointsInputs { @@ -41,7 +41,7 @@ object SearchEndpointsInputs { val default: Offset = unsafeFrom(0) } - final case class InputIri private(value: String) extends AnyVal + final case class InputIri private (value: String) extends AnyVal object InputIri { implicit val tapirCodec: Codec[String, InputIri, CodecFormat.TextPlain] = @@ -72,7 +72,7 @@ object SearchEndpointsInputs { final case class SearchEndpoints(baseEndpoints: BaseEndpoints) { - private val tags = List("v2", "search") + private val tags = List("v2", "search") private val gravsearchDescription = "The Gravsearch query. See https://docs.dasch.swiss/latest/DSP-API/03-endpoints/api-v2/query-language/" @@ -263,12 +263,12 @@ final case class SearchRestService( ) { def searchResourcesByLabelV2( - query: String, - opts: FormatOptions, - offset: Offset, - project: Option[ProjectIri], - limitByResourceClass: Option[InputIri], - user: UserADM + query: String, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + limitByResourceClass: Option[InputIri], + user: UserADM ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(limitByResourceClass.map(_.value))(iriConverter.asSmartIri) searchResult <- @@ -299,14 +299,14 @@ final case class SearchRestService( } yield response def fullTextSearch( - query: RenderedResponse, - opts: FormatOptions, - offset: Offset, - project: Option[ProjectIri], - resourceClass: Option[InputIri], - standoffClass: Option[InputIri], - returnFiles: Boolean, - user: UserADM + query: RenderedResponse, + opts: FormatOptions, + offset: Offset, + project: Option[ProjectIri], + resourceClass: Option[InputIri], + standoffClass: Option[InputIri], + returnFiles: Boolean, + user: UserADM ): Task[(RenderedResponse, MediaType)] = for { resourceClass <- ZIO.foreach(resourceClass.map(_.value))(iriConverter.asSmartIri) standoffClass <- ZIO.foreach(standoffClass.map(_.value))(iriConverter.asSmartIri) @@ -324,11 +324,11 @@ final case class SearchRestService( } yield response def fullTextSearchCount( - query: RenderedResponse, - opts: FormatOptions, - project: Option[ProjectIri], - resourceClass: Option[InputIri], - standoffClass: Option[InputIri] + query: RenderedResponse, + opts: FormatOptions, + project: Option[ProjectIri], + resourceClass: Option[InputIri], + standoffClass: Option[InputIri] ): zio.Task[ (_root_.org.knora.webapi.slice.common.api.KnoraResponseRenderer.RenderedResponse, _root_.sttp.model.MediaType) ] = for {