Skip to content

Commit e5be1db

Browse files
irinaschubertChristian KleinböltingBalduinLandoltmpro7
authored
refactor: refactor project route for ZIO HTTP (#2338)
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>
1 parent b06f5b4 commit e5be1db

6 files changed

Lines changed: 201 additions & 31 deletions

File tree

‎webapi/src/main/scala/org/knora/webapi/core/HttpServerWithZIOHttp.scala‎

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.knora.webapi.core
7+
import zhttp.service.Server
8+
import zio.ZLayer
9+
import zio._
10+
11+
import org.knora.webapi.config.AppConfig
12+
import org.knora.webapi.routing.admin.ProjectsRouteZ
13+
14+
object HttpServerZ {
15+
16+
val layer: ZLayer[AppRouter & AppConfig & State & ProjectsRouteZ, Nothing, Unit] =
17+
ZLayer {
18+
for {
19+
appConfig <- ZIO.service[AppConfig]
20+
projectsRoute <- ZIO.service[ProjectsRouteZ]
21+
r = projectsRoute.route
22+
port = appConfig.knoraApi.externalZioPort
23+
_ <- Server.start(port, r).forkDaemon
24+
_ <- ZIO.logInfo(">>> Acquire ZIO HTTP Server <<<")
25+
} yield ()
26+
}
27+
}

‎webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import zio.ZLayer
1111
import org.knora.webapi.auth.JWTService
1212
import org.knora.webapi.config.AppConfig
1313
import org.knora.webapi.routing.ApiRoutes
14+
import org.knora.webapi.routing.admin.ProjectsRouteZ
1415
import org.knora.webapi.store.cache.CacheServiceManager
1516
import org.knora.webapi.store.cache.api.CacheService
1617
import org.knora.webapi.store.cache.impl.CacheServiceInMemImpl
@@ -55,13 +56,14 @@ object LayersLive {
5556
CacheServiceManager.layer,
5657
CacheServiceInMemImpl.layer,
5758
HttpServer.layer,
58-
HttpServerWithZIOHttp.layer, // this is the new ZIO HTTP server layer
59+
HttpServerZ.layer, // this is the new ZIO HTTP server layer
5960
IIIFServiceManager.layer,
6061
IIIFServiceSipiImpl.layer,
6162
JWTService.layer,
6263
RepositoryUpdater.layer,
6364
State.layer,
6465
TriplestoreServiceManager.layer,
65-
TriplestoreServiceHttpConnectorImpl.layer
66+
TriplestoreServiceHttpConnectorImpl.layer,
67+
ProjectsRouteZ.layer
6668
)
6769
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.knora.webapi.http.handler
7+
8+
import spray.json._
9+
import zhttp.http.Http
10+
import zhttp.http.Response
11+
12+
import dsp.errors.RequestRejectedException
13+
import org.knora.webapi.config.AppConfig
14+
import org.knora.webapi.http.status.ApiStatusCodesZ
15+
16+
/**
17+
* Migrated from [[org.knora.webapi.http.handler.KnoraExceptionHandler]]
18+
*/
19+
object ExceptionHandlerZ {
20+
private val GENERIC_INTERNAL_SERVER_ERROR_MESSAGE =
21+
"The request could not be completed because of an internal server error."
22+
23+
def exceptionToJsonHttpResponseZ(ex: Throwable, appConfig: AppConfig): Http[Any, Nothing, Any, Response] = {
24+
25+
// Get the HTTP status code that corresponds to the exception.
26+
val httpStatus = ApiStatusCodesZ.fromExceptionZ(ex)
27+
28+
// Generate an HTTP response containing the error message ...
29+
val responseFields: Map[String, JsValue] = Map(
30+
"error" -> JsString(makeClientErrorMessage(ex, appConfig))
31+
)
32+
33+
val json = JsObject(responseFields).compactPrint
34+
35+
// ... and the HTTP status code.
36+
Http.response(Response.json(json).setStatus(httpStatus))
37+
38+
}
39+
40+
/**
41+
* Given an exception, returns an error message suitable for clients.
42+
*
43+
* @param ex the exception.
44+
* @param appConfig the application's configuration.
45+
* @return an error message suitable for clients.
46+
*/
47+
private def makeClientErrorMessage(ex: Throwable, appConfig: AppConfig): String =
48+
ex match {
49+
case rre: RequestRejectedException => rre.toString
50+
51+
case other =>
52+
if (appConfig.showInternalErrors) {
53+
other.toString
54+
} else {
55+
GENERIC_INTERNAL_SERVER_ERROR_MESSAGE
56+
}
57+
}
58+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.knora.webapi.http.status
7+
8+
import zhttp.http.Status
9+
10+
import dsp.errors._
11+
import org.knora.webapi.store.triplestore.errors.TriplestoreTimeoutException
12+
13+
/**
14+
* Migrated from [[org.knora.webapi.http.status.ApiStatusCodes]]
15+
*
16+
* The possible values for the HTTP status code that is returned as part of each Knora API v2 response.
17+
*/
18+
object ApiStatusCodesZ {
19+
20+
/**
21+
* Converts an exception to a suitable HTTP status code.
22+
*
23+
* @param ex an exception.
24+
* @return an HTTP status code.
25+
*/
26+
27+
def fromExceptionZ(ex: Throwable): Status =
28+
ex match {
29+
// Subclasses of RequestRejectedException
30+
case NotFoundException(_) => Status.NotFound
31+
case ForbiddenException(_) => Status.Forbidden
32+
case BadCredentialsException(_) => Status.Unauthorized
33+
case DuplicateValueException(_) => Status.BadRequest
34+
case OntologyConstraintException(_) => Status.BadRequest
35+
case EditConflictException(_) => Status.Conflict
36+
case BadRequestException(_) => Status.BadRequest
37+
case ValidationException(_, _) => Status.BadRequest
38+
case RequestRejectedException(_) => Status.BadRequest
39+
// RequestRejectedException must be the last one in this group
40+
41+
// Subclasses of InternalServerException
42+
case UpdateNotPerformedException(_) => Status.Conflict
43+
case TriplestoreTimeoutException(_, _) => Status.GatewayTimeout
44+
case InternalServerException(_) => Status.InternalServerError
45+
// InternalServerException must be the last one in this group
46+
47+
case _ => Status.InternalServerError
48+
}
49+
50+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.knora.webapi.routing.admin
2+
3+
import akka.actor.ActorRef
4+
import akka.pattern.ask
5+
import akka.util.Timeout
6+
import zhttp.http._
7+
import zio.ZIO
8+
import zio.ZLayer
9+
10+
import java.net.URLDecoder
11+
12+
import dsp.errors.BadRequestException
13+
import dsp.errors.InternalServerException
14+
import dsp.errors.KnoraException
15+
import dsp.errors.RequestRejectedException
16+
import org.knora.webapi.config.AppConfig
17+
import org.knora.webapi.core.AppRouter
18+
import org.knora.webapi.http.handler.ExceptionHandlerZ
19+
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetRequestADM
20+
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectGetResponseADM
21+
import org.knora.webapi.messages.admin.responder.projectsmessages.ProjectIdentifierADM
22+
import org.knora.webapi.messages.util.KnoraSystemInstances
23+
24+
final case class ProjectsRouteZ(router: AppRouter, appConfig: AppConfig) {
25+
implicit val sender: ActorRef = router.ref
26+
implicit val timeout: Timeout = appConfig.defaultTimeoutAsDuration
27+
28+
def getProjectByIri(iri: String): ZIO[Any, KnoraException, Response] =
29+
for {
30+
user <- ZIO.succeed(KnoraSystemInstances.Users.SystemUser)
31+
iriDecoded <-
32+
ZIO
33+
.attempt(URLDecoder.decode(iri, "utf-8"))
34+
.orElseFail(BadRequestException(s"Failed to decode IRI $iri"))
35+
iriValue <- ProjectIdentifierADM.IriIdentifier
36+
.fromString(iriDecoded)
37+
.toZIO
38+
message = ProjectGetRequestADM(identifier = iriValue, requestingUser = user)
39+
response <- ZIO.fromFuture(_ => router.ref.ask(message)).map(_.asInstanceOf[ProjectGetResponseADM]).orDie
40+
} yield Response.json(response.toJsValue.toString())
41+
42+
val route: HttpApp[Any, Nothing] =
43+
(Http
44+
.collectZIO[Request] {
45+
// TODO : Add user authentication, make tests run with the new route
46+
// Returns a single project identified through the IRI.
47+
case Method.GET -> !! / "admin" / "projects" / "iri" / iri =>
48+
getProjectByIri(iri)
49+
})
50+
.catchAll {
51+
case RequestRejectedException(e) =>
52+
ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
53+
case InternalServerException(e) =>
54+
ExceptionHandlerZ.exceptionToJsonHttpResponseZ(e, appConfig)
55+
}
56+
}
57+
58+
object ProjectsRouteZ {
59+
val layer: ZLayer[AppRouter with AppConfig, Nothing, ProjectsRouteZ] = ZLayer.fromFunction { (router, config) =>
60+
ProjectsRouteZ(router, config)
61+
}
62+
}

0 commit comments

Comments
 (0)