Skip to content

Commit

Permalink
Add route for getting information for resources /v2/resources/info
Browse files Browse the repository at this point in the history
includes orderBy [creationDate|lastmodficationDate], and order [ASC|DESC].

Http Example request:

GET /v2/resources/info?resourceClass=http%3A%2F%2F0.0.0.0%3A3333%2Fontology%2F0001%2Fanything%2Fv2%23Thing&order=DESC&orderBy=creationDate HTTP/1.1
X-Knora-Accept-Project: http://rdfh.ch/projects/Lw3FC39BSzCwvmdOaTyLqQ

Http Example response:
{
	"resources": [
		{
			"resourceIri": "http://rdfh.ch/0001/thing-with-pages",
			"creationDate": "2021-05-11T10:00:00Z",
			"lastModificationDate": "2021-05-11T10:00:00Z",
			"isDeleted": false
		},
		{
			"resourceIri": "http://rdfh.ch/0001/0JhgKcqoRIeRRG6ownArSw",
			"creationDate": "2020-04-07T09:12:56.710717Z",
			"lastModificationDate": "2020-04-07T09:12:56.710717Z",
			"isDeleted": false
		},
		...
	]
}
  • Loading branch information
seakayone committed Nov 29, 2022
1 parent 7ae6d24 commit eaae30b
Show file tree
Hide file tree
Showing 19 changed files with 488 additions and 97 deletions.
5 changes: 4 additions & 1 deletion webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package org.knora.webapi.core

import zio.ULayer
import zio.ZLayer

import org.knora.webapi.auth.JWTService
import org.knora.webapi.config.AppConfig
import org.knora.webapi.routing.ApiRoutes
import org.knora.webapi.routing.ApiRoutesWithZIOHttp
import org.knora.webapi.routing.HealthRouteWithZIOHttp
import org.knora.webapi.slice.resourceinfo.api.LiveRestResourceInfoService
import org.knora.webapi.slice.resourceinfo.repo.LiveResourceInfoRepo
import org.knora.webapi.store.cache.CacheServiceManager
import org.knora.webapi.store.cache.api.CacheService
import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl
Expand Down Expand Up @@ -58,6 +59,8 @@ object LayersLive {
IIIFServiceManager.layer,
IIIFServiceSipiImpl.layer,
JWTService.layer,
LiveResourceInfoRepo.layer,
LiveRestResourceInfoService.layer,
RepositoryUpdater.layer,
State.layer,
TriplestoreServiceManager.layer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ sealed trait SmartIri extends Ordered[SmartIri] with KnoraContentV2[SmartIri] {
*/
def toSparql: String

def toIri: IRI = toString
/**
* Returns `true` if this is a Knora data or definition IRI.
*/
Expand Down Expand Up @@ -485,6 +486,7 @@ sealed trait SmartIri extends Ordered[SmartIri] with KnoraContentV2[SmartIri] {
*/
override def toOntologySchema(targetSchema: OntologySchema): SmartIri

def internalIri: IRI = toOntologySchema(InternalSchema).toIri
/**
* Constructs a short prefix label for the ontology that the IRI belongs to.
*/
Expand Down
53 changes: 14 additions & 39 deletions webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,16 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import ch.megard.akka.http.cors.scaladsl.CorsDirectives
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
import zio._

import org.knora.webapi.config.AppConfig
import org.knora.webapi.core
import org.knora.webapi.core.ActorSystem
import org.knora.webapi.core.AppRouter
import org.knora.webapi.core.{ActorSystem, AppRouter}
import org.knora.webapi.http.directives.DSPApiDirectives
import org.knora.webapi.http.version.ServerVersion
import org.knora.webapi.routing.AroundDirectives
import org.knora.webapi.routing.HealthRoute
import org.knora.webapi.routing.KnoraRouteData
import org.knora.webapi.routing.RejectingRoute
import org.knora.webapi.routing.VersionRoute
import org.knora.webapi.routing.admin.FilesRouteADM
import org.knora.webapi.routing.admin.GroupsRouteADM
import org.knora.webapi.routing.admin.ListsRouteADM
import org.knora.webapi.routing.admin.PermissionsRouteADM
import org.knora.webapi.routing.admin.ProjectsRouteADM
import org.knora.webapi.routing.admin.StoreRouteADM
import org.knora.webapi.routing.admin.UsersRouteADM
import org.knora.webapi.routing.v1.AssetsRouteV1
import org.knora.webapi.routing.v1.AuthenticationRouteV1
import org.knora.webapi.routing.v1.CkanRouteV1
import org.knora.webapi.routing.v1.ListsRouteV1
import org.knora.webapi.routing.v1.ProjectsRouteV1
import org.knora.webapi.routing.v1.ResourceTypesRouteV1
import org.knora.webapi.routing.v1.ResourcesRouteV1
import org.knora.webapi.routing.v1.SearchRouteV1
import org.knora.webapi.routing.v1.StandoffRouteV1
import org.knora.webapi.routing.v1.UsersRouteV1
import org.knora.webapi.routing.v1.ValuesRouteV1
import org.knora.webapi.routing.v2.AuthenticationRouteV2
import org.knora.webapi.routing.v2.ListsRouteV2
import org.knora.webapi.routing.v2.OntologiesRouteV2
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.routing.admin._
import org.knora.webapi.routing.v1._
import org.knora.webapi.routing.v2._
import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService
import zio._

trait ApiRoutes {
val routes: Route
Expand All @@ -57,7 +29,7 @@ object ApiRoutes {
/**
* All routes composed together.
*/
val layer: ZLayer[ActorSystem & AppRouter & core.State & AppConfig, Nothing, ApiRoutes] =
val layer: ZLayer[ActorSystem & AppRouter & core.State & AppConfig with RestResourceInfoService, Nothing, ApiRoutes] =
ZLayer {
for {
sys <- ZIO.service[ActorSystem]
Expand All @@ -70,7 +42,7 @@ object ApiRoutes {
appConfig = appConfig
)
)
runtime <- ZIO.runtime[core.State]
runtime <- ZIO.runtime[core.State with RestResourceInfoService]
} yield ApiRoutesImpl(routeData, runtime, appConfig)
}
}
Expand All @@ -82,8 +54,11 @@ object ApiRoutes {
* ALL requests go through each of the routes in ORDER.
* The FIRST matching route is used for handling a request.
*/
private final case class ApiRoutesImpl(routeData: KnoraRouteData, runtime: Runtime[core.State], appConfig: AppConfig)
extends ApiRoutes
private final case class ApiRoutesImpl(
routeData: KnoraRouteData,
runtime: Runtime[core.State with RestResourceInfoService],
appConfig: AppConfig
) extends ApiRoutes
with AroundDirectives {

val routes =
Expand All @@ -108,7 +83,7 @@ private final case class ApiRoutesImpl(routeData: KnoraRouteData, runtime: Runti
new ProjectsRouteV1(routeData).makeRoute ~
new OntologiesRouteV2(routeData).makeRoute ~
new SearchRouteV2(routeData).makeRoute ~
new ResourcesRouteV2(routeData).makeRoute ~
new ResourcesRouteV2(routeData, runtime).makeRoute ~
new ValuesRouteV2(routeData).makeRoute ~
new StandoffRouteV2(routeData).makeRoute ~
new ListsRouteV2(routeData).makeRoute ~
Expand Down
15 changes: 14 additions & 1 deletion webapi/src/main/scala/org/knora/webapi/routing/RouteUtilV2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import com.typesafe.scalalogging.Logger
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.control.Exception.catching

import dsp.errors.BadRequestException
import dsp.errors.UnexpectedMessageException
import org.knora.webapi._
Expand All @@ -32,6 +31,8 @@ import org.knora.webapi.messages.util.rdf.RdfModel
import org.knora.webapi.messages.v2.responder.KnoraResponseV2
import org.knora.webapi.messages.v2.responder.resourcemessages.ResourceTEIGetResponseV2

import scala.util.{Failure, Success, Try}

/**
* Handles message formatting, content negotiation, and simple interactions with responders, on behalf of Knora routes.
*/
Expand Down Expand Up @@ -198,6 +199,18 @@ object RouteUtilV2 {
projectIriStr.toSmartIriWithErr(throw BadRequestException(s"Invalid project IRI: $projectIriStr"))
}

/**
* Gets the project IRI specified in a Knora-specific HTTP header.
*
* @param ctx the akka-http [[RequestContext]].
* @return the [[Try]] contains the specified project IRI, or if invalid a BadRequestException
*/
def getRequiredProjectFromHeader(ctx: RequestContext)(implicit stringFormatter: StringFormatter): Try[SmartIri] =
getProject(ctx) match {
case None => Failure(BadRequestException(s"This route requires the request header ${RouteUtilV2.PROJECT_HEADER}"))
case Some(value) => Success(value)
}

/**
* Sends a message to a responder and completes the HTTP request by returning the response as RDF using content negotiation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,35 @@

package org.knora.webapi.routing.v2

import akka.http.scaladsl.model.ContentTypes.`application/json`
import akka.http.scaladsl.model.StatusCodes.{InternalServerError, OK}
import akka.http.scaladsl.model.{HttpEntity, HttpResponse}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.PathMatcher
import akka.http.scaladsl.server.Route

import java.time.Instant
import java.util.UUID
import scala.concurrent.Future

import akka.http.scaladsl.server.{PathMatcher, RequestContext, Route}
import dsp.errors.BadRequestException
import org.knora.webapi._
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.SmartIri
import org.knora.webapi.messages.StringFormatter
import org.knora.webapi.messages.util.rdf.JsonLDDocument
import org.knora.webapi.messages.util.rdf.JsonLDUtil
import org.knora.webapi.messages.util.rdf.{JsonLDDocument, 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.ArchiveFileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.AudioFileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.DocumentFileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.FileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.MovingImageFileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.StillImageFileValueContentV2
import org.knora.webapi.messages.v2.responder.valuemessages.TextFileValueContentV2
import org.knora.webapi.routing.Authenticator
import org.knora.webapi.routing.KnoraRoute
import org.knora.webapi.routing.KnoraRouteData
import org.knora.webapi.routing.RouteUtilV2
import org.knora.webapi.messages.v2.responder.valuemessages._
import org.knora.webapi.messages.{SmartIri, StringFormatter}
import org.knora.webapi.routing.RouteUtilV2.getRequiredProjectFromHeader
import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilV2}
import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService
import org.knora.webapi.slice.resourceinfo.repo.ResourceInfoRepo.{ASC, Order, OrderBy, lastModificationDate}
import zio.Exit.{Failure, Success}
import zio.json._
import zio.{Exit, Runtime, Unsafe, ZIO}

import java.time.Instant
import java.util.UUID
import scala.concurrent.Future

/**
* Provides a routing function for API v2 routes that deal with resources.
*/
class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator {
class ResourcesRouteV2(routeData: KnoraRouteData, implicit val runtime: zio.Runtime[RestResourceInfoService]) extends KnoraRoute(routeData) with Authenticator {

val resourcesBasePath: PathMatcher[Unit] = PathMatcher("v2" / "resources")

Expand All @@ -63,6 +59,7 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
getResourceHistory() ~
getResourceHistoryEvents() ~
getProjectResourceAndValueHistory() ~
getResourcesInfo ~
getResources() ~
getResourcesPreview() ~
getResourcesTei() ~
Expand Down Expand Up @@ -338,6 +335,52 @@ class ResourcesRouteV2(routeData: KnoraRouteData) extends KnoraRoute(routeData)
}
}

private def getQueryParamsMap(requestContext: RequestContext): Map[String, String] =
requestContext.request.uri.query().toMap

private def getStringQueryParam(requestContext: RequestContext, key: String): Option[String] =
getQueryParamsMap(requestContext).get(key)

private def getRequiredStringQueryParam(requestContext: RequestContext, key: String): String =
getQueryParamsMap(requestContext).getOrElse(
key,
throw BadRequestException(s"This route requires the parameter '$key'")
)

private def getRequiredResourceClassFromQueryParams(ctx: RequestContext): SmartIri = {
val resourceClass = getRequiredStringQueryParam(ctx, "resourceClass")
resourceClass
.toSmartIriWithErr(throw BadRequestException(s"Invalid resource class IRI: $resourceClass"))
}

private def unsafeRunZioAndMapJsonResponse[R, E, A](
zioAction: ZIO[R, E, A]
)(implicit r: Runtime[R], encoder: JsonEncoder[A]) =
unsafeRunZio(zioAction) match {
case Failure(cause) => log.error(cause.prettyPrint); HttpResponse(InternalServerError)
case Success(dto) => HttpResponse(status = OK, entity = HttpEntity(`application/json`, dto.toJson))
}

private def unsafeRunZio[R, E, A](zioAction: ZIO[R, E, A])(implicit r: Runtime[R]): Exit[E, A] =
Unsafe.unsafe(implicit u => r.unsafe.run(zioAction))

private def getResourcesInfo: Route = path(resourcesBasePath / "info") {
get { ctx =>
val projectIri = getRequiredProjectFromHeader(ctx).get.internalIri
val resourceClassIri = getRequiredResourceClassFromQueryParams(ctx).internalIri
val orderBy = getStringQueryParam(ctx, "orderBy") match {
case None => lastModificationDate
case Some(s) => OrderBy.make(s).getOrElse(throw BadRequestException(s"Invalid value '$s', for orderBy"))
}
val order: Order = getStringQueryParam(ctx, "order") match {
case None => ASC
case Some(s) => Order.make(s).getOrElse(throw BadRequestException(s"Invalid value '$s', for order"))
}
val action =
RestResourceInfoService.findByProjectAndResourceClass(projectIri, resourceClassIri, (orderBy, order))
ctx.complete(unsafeRunZioAndMapJsonResponse(action))
}
}
private def getResources(): Route = path(resourcesBasePath / Segments) { resIris: Seq[String] =>
get { requestContext =>
if (resIris.size > routeData.appConfig.v2.resourcesSequence.resultsPerPage)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.knora.webapi.slice.resourceinfo.api

import org.knora.webapi.IRI
import org.knora.webapi.slice.resourceinfo.repo.ResourceInfoRepo
import org.knora.webapi.slice.resourceinfo.repo.ResourceInfoRepo.{Order, OrderBy}
import zio.{UIO, ZIO, ZLayer}

final case class LiveRestResourceInfoService(repo: ResourceInfoRepo) extends RestResourceInfoService {
override def findByProjectAndResourceClass(
projectIri: IRI,
resourceClass: IRI,
ordering: (OrderBy, Order)
): UIO[ListResponseDto] =
for {
result <- repo.findByProjectAndResourceClass(projectIri, resourceClass, ordering).map(_.map(ResourceInfoDto(_)))
} yield ListResponseDto(result)
}

object LiveRestResourceInfoService {
val layer: ZLayer[ResourceInfoRepo, Nothing, LiveRestResourceInfoService] =
ZLayer.fromZIO(ZIO.service[ResourceInfoRepo].map(new LiveRestResourceInfoService(_)))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.knora.webapi.slice.resourceinfo.api

import org.knora.webapi.IRI
import org.knora.webapi.slice.resourceinfo.repo.ResourceInfo
import zio.json._

import java.time.Instant

final case class ListResponseDto private (resources: List[ResourceInfoDto], count: Int)
object ListResponseDto {
val empty: ListResponseDto = ListResponseDto(List.empty, 0)
def apply(list: List[ResourceInfoDto]): ListResponseDto = list match {
case Nil => ListResponseDto.empty
case list => ListResponseDto(list, list.size)
}

implicit val encoder: JsonEncoder[ListResponseDto] =
DeriveJsonEncoder.gen[ListResponseDto]
}

final case class ResourceInfoDto private (
resourceIri: IRI,
creationDate: Instant,
lastModificationDate: Instant,
deleteDate: Option[Instant],
isDeleted: Boolean
)
object ResourceInfoDto {
def apply(info: ResourceInfo): ResourceInfoDto =
ResourceInfoDto(info.iri, info.creationDate, info.lastModificationDate, info.deleteDate, info.isDeleted)

implicit val encoder: JsonEncoder[ResourceInfoDto] =
DeriveJsonEncoder.gen[ResourceInfoDto]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.knora.webapi.slice.resourceinfo.api

import org.knora.webapi.IRI
import org.knora.webapi.slice.resourceinfo.repo.ResourceInfoRepo.{ASC, Order, OrderBy, lastModificationDate}
import zio.{UIO, ZIO}

trait RestResourceInfoService {
def findByProjectAndResourceClass(
projectIri: IRI,
resourceClass: IRI,
ordering: (OrderBy, Order)
): UIO[ListResponseDto]
}

object RestResourceInfoService {
def findByProjectAndResourceClass(
projectIri: IRI,
resourceClass: IRI,
ordering: (OrderBy, Order) = (lastModificationDate, ASC)
): ZIO[RestResourceInfoService, Nothing, ListResponseDto] =
ZIO.service[RestResourceInfoService].flatMap(_.findByProjectAndResourceClass(projectIri, resourceClass, ordering))
}

0 comments on commit eaae30b

Please sign in to comment.