diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServerWithZIOHttp.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServerWithZIOHttp.scala deleted file mode 100644 index 022b11d6ef..0000000000 --- a/webapi/src/main/scala/org/knora/webapi/core/HttpServerWithZIOHttp.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.IndexApp - -object HttpServerWithZIOHttp { - - val routes = IndexApp() - - val layer: ZLayer[AppConfig & State, Nothing, Unit] = - ZLayer { - for { - appConfig <- ZIO.service[AppConfig] - port = appConfig.knoraApi.externalZioPort - _ <- Server.start(port, routes).forkDaemon - _ <- ZIO.logInfo(">>> Acquire ZIO HTTP Server <<<") - } yield () - } - -} diff --git a/webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala b/webapi/src/main/scala/org/knora/webapi/core/HttpServerZ.scala new file mode 100644 index 0000000000..9cf0e36383 --- /dev/null +++ b/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 () + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index d45fb48dc0..b094819d48 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -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 @@ -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 ) } diff --git a/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala b/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala new file mode 100644 index 0000000000..b7890917e5 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/http/handler/ExceptionHandlerZ.scala @@ -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 + } + } +} diff --git a/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala new file mode 100644 index 0000000000..05da7c1095 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/http/status/ApiStatusCodesZ.scala @@ -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 + } + +} diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala new file mode 100644 index 0000000000..69b1ddc3bb --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/ProjectsRouteZ.scala @@ -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) + } +}