Skip to content

Commit

Permalink
refactor: refactor project route for ZIO HTTP (#2338)
Browse files Browse the repository at this point in the history
Co-authored-by: Christian Kleinbölting <christian.kleinboelting@dasch.swiss>
Co-authored-by: Balduin Landolt <33053745+BalduinLandolt@users.noreply.github.com>
Co-authored-by: Marcin Procyk <marcin.procyk@dasch.swiss>
  • Loading branch information
4 people committed Dec 15, 2022
1 parent b06f5b4 commit e5be1db
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 31 deletions.

This file was deleted.

27 changes: 27 additions & 0 deletions webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala
@@ -0,0 +1,27 @@
/*
* Copyright © 2021 - 2022 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.core
import zhttp.service.Server
import zio.ZLayer
import zio._

import org.knora.webapi.config.AppConfig
import org.knora.webapi.routing.admin.ProjectsRouteZ

object HttpServerZ {

val layer: ZLayer[AppRouter & AppConfig & State & ProjectsRouteZ, Nothing, Unit] =
ZLayer {
for {
appConfig <- ZIO.service[AppConfig]
projectsRoute <- ZIO.service[ProjectsRouteZ]
r = projectsRoute.route
port = appConfig.knoraApi.externalZioPort
_ <- Server.start(port, r).forkDaemon
_ <- ZIO.logInfo(">>> Acquire ZIO HTTP Server <<<")
} yield ()
}
}
6 changes: 4 additions & 2 deletions webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala
Expand Up @@ -11,6 +11,7 @@ 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.admin.ProjectsRouteZ
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 @@ -55,13 +56,14 @@ object LayersLive {
CacheServiceManager.layer,
CacheServiceInMemImpl.layer,
HttpServer.layer,
HttpServerWithZIOHttp.layer, // this is the new ZIO HTTP server layer
HttpServerZ.layer, // this is the new ZIO HTTP server layer
IIIFServiceManager.layer,
IIIFServiceSipiImpl.layer,
JWTService.layer,
RepositoryUpdater.layer,
State.layer,
TriplestoreServiceManager.layer,
TriplestoreServiceHttpConnectorImpl.layer
TriplestoreServiceHttpConnectorImpl.layer,
ProjectsRouteZ.layer
)
}
@@ -0,0 +1,58 @@
/*
* Copyright © 2021 - 2022 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.http.handler

import spray.json._
import zhttp.http.Http
import zhttp.http.Response

import dsp.errors.RequestRejectedException
import org.knora.webapi.config.AppConfig
import org.knora.webapi.http.status.ApiStatusCodesZ

/**
* Migrated from [[org.knora.webapi.http.handler.KnoraExceptionHandler]]
*/
object ExceptionHandlerZ {
private val GENERIC_INTERNAL_SERVER_ERROR_MESSAGE =
"The request could not be completed because of an internal server error."

def exceptionToJsonHttpResponseZ(ex: Throwable, appConfig: AppConfig): Http[Any, Nothing, Any, Response] = {

// Get the HTTP status code that corresponds to the exception.
val httpStatus = ApiStatusCodesZ.fromExceptionZ(ex)

// Generate an HTTP response containing the error message ...
val responseFields: Map[String, JsValue] = Map(
"error" -> JsString(makeClientErrorMessage(ex, appConfig))
)

val json = JsObject(responseFields).compactPrint

// ... and the HTTP status code.
Http.response(Response.json(json).setStatus(httpStatus))

}

/**
* Given an exception, returns an error message suitable for clients.
*
* @param ex the exception.
* @param appConfig the application's configuration.
* @return an error message suitable for clients.
*/
private def makeClientErrorMessage(ex: Throwable, appConfig: AppConfig): String =
ex match {
case rre: RequestRejectedException => rre.toString

case other =>
if (appConfig.showInternalErrors) {
other.toString
} else {
GENERIC_INTERNAL_SERVER_ERROR_MESSAGE
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright © 2021 - 2022 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.http.status

import zhttp.http.Status

import dsp.errors._
import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException

/**
* Migrated from [[org.knora.webapi.http.status.ApiStatusCodes]]
*
* The possible values for the HTTP status code that is returned as part of each Knora API v2 response.
*/
object ApiStatusCodesZ {

/**
* Converts an exception to a suitable HTTP status code.
*
* @param ex an exception.
* @return an HTTP status code.
*/

def fromExceptionZ(ex: Throwable): Status =
ex match {
// Subclasses of RequestRejectedException
case NotFoundException(_) => Status.NotFound
case ForbiddenException(_) => Status.Forbidden
case BadCredentialsException(_) => Status.Unauthorized
case DuplicateValueException(_) => Status.BadRequest
case OntologyConstraintException(_) => Status.BadRequest
case EditConflictException(_) => Status.Conflict
case BadRequestException(_) => Status.BadRequest
case ValidationException(_, _) => Status.BadRequest
case RequestRejectedException(_) => Status.BadRequest
// RequestRejectedException must be the last one in this group

// Subclasses of InternalServerException
case UpdateNotPerformedException(_) => Status.Conflict
case TriplestoreTimeoutException(_, _) => Status.GatewayTimeout
case InternalServerException(_) => Status.InternalServerError
// InternalServerException must be the last one in this group

case _ => Status.InternalServerError
}

}
@@ -0,0 +1,62 @@
package org.knora.webapi.routing.admin

import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import zhttp.http._
import zio.ZIO
import zio.ZLayer

import java.net.URLDecoder

import dsp.errors.BadRequestException
import dsp.errors.InternalServerException
import dsp.errors.KnoraException
import dsp.errors.RequestRejectedException
import org.knora.webapi.config.AppConfig
import org.knora.webapi.core.AppRouter
import org.knora.webapi.http.handler.ExceptionHandlerZ
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
import org.knora.webapi.messages.util.KnoraSystemInstances

final case class ProjectsRouteZ(router: AppRouter, appConfig: AppConfig) {
implicit val sender: ActorRef = router.ref
implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration

def getProjectByIri(iri: String): ZIO[Any, KnoraException, Response] =
for {
user <- ZIO.succeed(KnoraSystemInstances.Users.SystemUser)
iriDecoded <-
ZIO
.attempt(URLDecoder.decode(iri, "utf-8"))
.orElseFail(BadRequestException(s"Failed to decode IRI $iri"))
iriValue <- ProjectIdentifierADM.IriIdentifier
.fromString(iriDecoded)
.toZIO
message = ProjectGetRequestADM(identifier = iriValue, requestingUser = user)
response <- ZIO.fromFuture(_ => router.ref.ask(message)).map(_.asInstanceOf[ProjectGetResponseADM]).orDie
} yield Response.json(response.toJsValue.toString())

val route: HttpApp[Any, Nothing] =
(Http
.collectZIO[Request] {
// TODO : Add user authentication, make tests run with the new route
// Returns a single project identified through the IRI.
case Method.GET -> !! / "admin" / "projects" / "iri" / iri =>
getProjectByIri(iri)
})
.catchAll {
case RequestRejectedException(e) =>
ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
case InternalServerException(e) =>
ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
}
}

object ProjectsRouteZ {
val layer: ZLayer[AppRouter with AppConfig, Nothing, ProjectsRouteZ] = ZLayer.fromFunction { (router, config) =>
ProjectsRouteZ(router, config)
}
}

0 comments on commit e5be1db

Please sign in to comment.