diff --git a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala index 4e39a94a3a..579154f748 100644 --- a/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala +++ b/integration/src/test/scala/org/knora/webapi/core/LayersTest.scala @@ -30,16 +30,13 @@ import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive import org.knora.webapi.routing._ import org.knora.webapi.routing.admin.AuthenticatorService import org.knora.webapi.routing.admin.ProjectsRouteZ -import org.knora.webapi.slice.admin.api.ProjectsEndpoints -import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler +import org.knora.webapi.slice.admin.api._ +import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive import org.knora.webapi.slice.admin.domain.service._ import org.knora.webapi.slice.admin.repo.service.KnoraProjectRepoLive -import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapperF -import org.knora.webapi.slice.common.api.RestPermissionService -import org.knora.webapi.slice.common.api.RestPermissionServiceLive +import org.knora.webapi.slice.common.api._ import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.ontology.api.service.RestCardinalityServiceLive @@ -135,7 +132,7 @@ object LayersTest { private val commonLayersForAllIntegrationTests = ZLayer.makeSome[CommonR0, CommonR]( - HandlerMapperF.layer, + AdminApiRoutes.layer, ApiRoutes.layer, AppRouter.layer, AuthenticationMiddleware.layer, @@ -151,6 +148,7 @@ object LayersTest { DspIngestClientLive.layer, GravsearchTypeInspectionRunner.layer, GroupsResponderADMLive.layer, + HandlerMapper.layer, HttpServer.layer, HttpServerZ.layer, IIIFRequestMessageHandlerLive.layer, @@ -160,6 +158,10 @@ object LayersTest { KnoraProjectRepoLive.layer, ListsResponderADMLive.layer, ListsResponderV2Live.layer, + MaintenanceEndpoints.layer, + MaintenanceEndpointsHandlers.layer, + MaintenanceRestService.layer, + MaintenanceServiceLive.layer, MessageRelayLive.layer, OntologyCacheLive.layer, OntologyHelpersLive.layer, @@ -194,6 +196,7 @@ object LayersTest { StandoffTagUtilV2Live.layer, State.layer, StoresResponderADMLive.layer, + TapirToPekkoInterpreter.layer, TestClientService.layer, TriplestoreServiceLive.layer, UsersResponderADMLive.layer, 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 2585dfca5c..01640bcde9 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -30,16 +30,13 @@ import org.knora.webapi.responders.v2.ontology.OntologyHelpersLive import org.knora.webapi.routing._ import org.knora.webapi.routing.admin.AuthenticatorService import org.knora.webapi.routing.admin.ProjectsRouteZ -import org.knora.webapi.slice.admin.api.ProjectsEndpoints -import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler +import org.knora.webapi.slice.admin.api._ +import org.knora.webapi.slice.admin.api.service.MaintenanceRestService import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.admin.api.service.ProjectsADMRestServiceLive import org.knora.webapi.slice.admin.domain.service._ import org.knora.webapi.slice.admin.repo.service.KnoraProjectRepoLive -import org.knora.webapi.slice.common.api.BaseEndpoints -import org.knora.webapi.slice.common.api.HandlerMapperF -import org.knora.webapi.slice.common.api.RestPermissionService -import org.knora.webapi.slice.common.api.RestPermissionServiceLive +import org.knora.webapi.slice.common.api._ import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.ontology.api.service.RestCardinalityServiceLive @@ -134,6 +131,7 @@ object LayersLive { val dspLayersLive: ULayer[DspEnvironmentLive] = ZLayer.make[DspEnvironmentLive]( ActorSystem.layer, + AdminApiRoutes.layer, ApiRoutes.layer, AppConfig.layer, AppRouter.layer, @@ -150,7 +148,7 @@ object LayersLive { DspIngestClientLive.layer, GravsearchTypeInspectionRunner.layer, GroupsResponderADMLive.layer, - HandlerMapperF.layer, + HandlerMapper.layer, HttpServer.layer, HttpServerZ.layer, // this is the new ZIO HTTP server layer IIIFRequestMessageHandlerLive.layer, @@ -162,6 +160,10 @@ object LayersLive { KnoraProjectRepoLive.layer, ListsResponderADMLive.layer, ListsResponderV2Live.layer, + MaintenanceEndpoints.layer, + MaintenanceEndpointsHandlers.layer, + MaintenanceRestService.layer, + MaintenanceServiceLive.layer, MessageRelayLive.layer, OntologyCacheLive.layer, OntologyHelpersLive.layer, @@ -197,6 +199,7 @@ object LayersLive { State.layer, StoresResponderADMLive.layer, StringFormatter.live, + TapirToPekkoInterpreter.layer, TriplestoreServiceLive.layer, UsersResponderADMLive.layer, ValuesResponderV2Live.layer diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala index 5f13feab0c..a529a6f32a 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala @@ -137,7 +137,7 @@ trait ProjectsResponderADM { /** * Creates a project. * - * @param projectCreate the new project's information. + * @param createReq the new project's information. * @param requestingUser the user that is making the request. * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. @@ -149,7 +149,7 @@ trait ProjectsResponderADM { * [[BadRequestException]] In the case when the shortcode is invalid. */ def projectCreateRequestADM( - projectCreate: ProjectCreateRequest, + createReq: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] @@ -157,17 +157,17 @@ trait ProjectsResponderADM { /** * Update project's basic information. * - * @param projectIri the IRI of the project. - * @param projectUpdate the update payload. - * @param user the user making the request. - * @param apiRequestID the unique api request ID. + * @param projectIri the IRI of the project. + * @param updateReq the update payload. + * @param user the user making the request. + * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. * * [[ForbiddenException]] In the case that the user is not allowed to perform the operation. */ def changeBasicInformationRequestADM( projectIri: Iri.ProjectIri, - projectUpdate: ProjectUpdateRequest, + updateReq: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] @@ -457,17 +457,17 @@ final case class ProjectsResponderADMLive( /** * Update project's basic information. * - * @param projectIri the IRI of the project. - * @param projectUpdate the update payload. - * @param user the user making the request. - * @param apiRequestID the unique api request ID. + * @param projectIri the IRI of the project. + * @param updateReq the update payload. + * @param user the user making the request. + * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. * * [[ForbiddenException]] In the case that the user is not allowed to perform the operation. */ override def changeBasicInformationRequestADM( projectIri: Iri.ProjectIri, - projectUpdate: ProjectUpdateRequest, + updateReq: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = { @@ -477,17 +477,17 @@ final case class ProjectsResponderADMLive( */ def changeProjectTask( projectIri: Iri.ProjectIri, - projectUpdatePayload: ProjectUpdateRequest, + updateReq: ProjectUpdateRequest, requestingUser: UserADM ): Task[ProjectOperationResponseADM] = // check if the requesting user is allowed to perform updates if (!requestingUser.permissions.isProjectAdmin(projectIri.value) && !requestingUser.permissions.isSystemAdmin) { ZIO.fail(ForbiddenException("Project's information can only be changed by a project or system admin.")) } else { - updateProjectADM(projectIri, projectUpdatePayload) + updateProjectADM(projectIri, updateReq) } - val task = changeProjectTask(projectIri, projectUpdate, user) + val task = changeProjectTask(projectIri, updateReq, user) IriLocker.runWithIriLock(apiRequestID, projectIri.value, task) } @@ -652,8 +652,7 @@ final case class ProjectsResponderADMLive( /** * Creates a project. * - * @param projectCreate the new project's information. - * + * @param createReq the new project's information. * @param requestingUser the user that is making the request. * @param apiRequestID the unique api request ID. * @return A [[ProjectOperationResponseADM]]. @@ -665,7 +664,7 @@ final case class ProjectsResponderADMLive( * [[BadRequestException]] In the case when the shortcode is invalid. */ override def projectCreateRequestADM( - projectCreate: ProjectCreateRequest, + createReq: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = { @@ -819,7 +818,7 @@ final case class ProjectsResponderADMLive( } yield ProjectOperationResponseADM(project = newProjectADM.unescape) - val task = projectCreateTask(projectCreate, requestingUser) + val task = projectCreateTask(createReq, requestingUser) IriLocker.runWithIriLock(apiRequestID, PROJECTS_GLOBAL_LOCK_IRI, task) } 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 0139c27db4..ca225476e3 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/ApiRoutes.scala @@ -5,19 +5,14 @@ package org.knora.webapi.routing -import org.apache.pekko +import org.apache.pekko.actor import org.apache.pekko.http.cors.scaladsl.CorsDirectives -import org.apache.pekko.http.scaladsl.model.HttpMethods.DELETE -import org.apache.pekko.http.scaladsl.model.HttpMethods.GET -import org.apache.pekko.http.scaladsl.model.HttpMethods.HEAD -import org.apache.pekko.http.scaladsl.model.HttpMethods.OPTIONS -import org.apache.pekko.http.scaladsl.model.HttpMethods.PATCH -import org.apache.pekko.http.scaladsl.model.HttpMethods.POST -import org.apache.pekko.http.scaladsl.model.HttpMethods.PUT +import org.apache.pekko.http.cors.scaladsl.settings.CorsSettings +import org.apache.pekko.http.scaladsl.model.HttpMethods._ +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route import zio._ -import scala.concurrent.ExecutionContextExecutor - import org.knora.webapi.config.AppConfig import org.knora.webapi.core import org.knora.webapi.core.ActorSystem @@ -30,19 +25,14 @@ import org.knora.webapi.responders.v2.ValuesResponderV2 import org.knora.webapi.routing import org.knora.webapi.routing.admin._ import org.knora.webapi.routing.v2._ +import org.knora.webapi.slice.admin.api.AdminApiRoutes import org.knora.webapi.slice.admin.api.ProjectsEndpointsHandler import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo -import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter import org.knora.webapi.slice.ontology.api.service.RestCardinalityService import org.knora.webapi.slice.resourceinfo.api.RestResourceInfoService import org.knora.webapi.slice.resourceinfo.domain.IriConverter -import pekko.actor -import pekko.http.scaladsl.server.Directives._ -import pekko.http.scaladsl.server.Route -import pekko.http.cors.scaladsl.settings.CorsSettings - trait ApiRoutes { val routes: Route } @@ -54,6 +44,7 @@ object ApiRoutes { */ val layer: URLayer[ ActorSystem + with AdminApiRoutes with AppConfig with AppRouter with IriConverter @@ -71,12 +62,11 @@ object ApiRoutes { ] = ZLayer { for { - sys <- ZIO.service[ActorSystem] - router <- ZIO.service[AppRouter] - appConfig <- ZIO.service[AppConfig] - projectsHandler <- ZIO.service[ProjectsEndpointsHandler] - routeData <- ZIO.succeed(KnoraRouteData(sys.system, router.ref, appConfig)) - tapirToPekkoRoute = TapirToPekkoInterpreter()(sys.system.dispatcher) + sys <- ZIO.service[ActorSystem] + router <- ZIO.service[AppRouter] + appConfig <- ZIO.service[AppConfig] + adminApiRoutes <- ZIO.service[AdminApiRoutes] + routeData <- ZIO.succeed(KnoraRouteData(sys.system, router.ref, appConfig)) runtime <- ZIO.runtime[ AppConfig with IriConverter @@ -90,7 +80,7 @@ object ApiRoutes { with core.State with routing.Authenticator ] - } yield ApiRoutesImpl(routeData, projectsHandler, tapirToPekkoRoute, appConfig, runtime) + } yield ApiRoutesImpl(routeData, adminApiRoutes, appConfig, runtime) } } @@ -103,8 +93,7 @@ object ApiRoutes { */ private final case class ApiRoutesImpl( routeData: KnoraRouteData, - projectsHandler: ProjectsEndpointsHandler, - tapirToPekkoRoute: TapirToPekkoInterpreter, + adminApiRoutes: AdminApiRoutes, appConfig: AppConfig, implicit val runtime: Runtime[ AppConfig @@ -122,8 +111,7 @@ private final case class ApiRoutesImpl( ) extends ApiRoutes with AroundDirectives { - implicit val system: actor.ActorSystem = routeData.system - implicit val executionContext: ExecutionContextExecutor = routeData.system.dispatcher + private implicit val system: actor.ActorSystem = routeData.system val routes: Route = logDuration { @@ -134,8 +122,7 @@ private final case class ApiRoutesImpl( .withAllowedMethods(List(GET, PUT, POST, DELETE, PATCH, HEAD, OPTIONS)) ) { DSPApiDirectives.handleErrors(appConfig) { - val adminProjectsRoutes = projectsHandler.allHanders.map(tapirToPekkoRoute.toRoute(_)).reduce(_ ~ _) - adminProjectsRoutes ~ + adminApiRoutes.routes.reduce(_ ~ _) ~ AuthenticationRouteV2().makeRoute ~ FilesRouteADM(routeData, runtime).makeRoute ~ GroupsRouteADM(routeData, runtime).makeRoute ~ diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala new file mode 100644 index 0000000000..6d3fcb8f90 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/AdminApiRoutes.scala @@ -0,0 +1,26 @@ +/* + * 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.slice.admin.api + +import org.apache.pekko.http.scaladsl.server.Route +import zio.ZLayer + +import org.knora.webapi.slice.common.api.TapirToPekkoInterpreter + +final case class AdminApiRoutes( + maintenance: MaintenanceEndpointsHandlers, + project: ProjectsEndpointsHandler, + tapirToPekko: TapirToPekkoInterpreter +) { + + private val handlers = maintenance.handlers ++ project.allHanders + + val routes: Seq[Route] = handlers.map(tapirToPekko.toRoute(_)) +} + +object AdminApiRoutes { + val layer = ZLayer.derive[AdminApiRoutes] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala new file mode 100644 index 0000000000..69490ba5a6 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpoints.scala @@ -0,0 +1,43 @@ +/* + * 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.slice.admin.api + +import sttp.model.StatusCode +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.json.zio.{jsonBody => zioJsonBody} +import zio.ZLayer +import zio.json.ast.Json + +import org.knora.webapi.slice.common.api.BaseEndpoints + +final case class MaintenanceEndpoints(baseEndpoints: BaseEndpoints) { + + private val maintenanceBase = "admin" / "maintenance" + + val postMaintenance = baseEndpoints.securedEndpoint.post + .in( + maintenanceBase / path[String] + .name("Maintenance action name") + .description(""" + |The name of the maintenance action to be executed. + |Maintenance actions are executed asynchronously in the background. + |""".stripMargin) + .example("fix-top-left-dimensions") + ) + .in( + zioJsonBody[Option[Json]] + .description(""" + |The optional parameters as json for the maintenance action. + |May be required by certain actions. + |""".stripMargin) + ) + .out(statusCode(StatusCode.Accepted)) +} + +object MaintenanceEndpoints { + val layer = ZLayer.derive[MaintenanceEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala new file mode 100644 index 0000000000..e80c1d9694 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/MaintenanceEndpointsHandlers.scala @@ -0,0 +1,35 @@ +/* + * 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.slice.admin.api + +import zio.ZLayer +import zio.json.ast.Json + +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.slice.admin.api.service.MaintenanceRestService +import org.knora.webapi.slice.common.api.HandlerMapper +import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler + +final case class MaintenanceEndpointsHandlers( + endpoints: MaintenanceEndpoints, + restService: MaintenanceRestService, + mapper: HandlerMapper +) { + + private val postMaintenanceHandler = + SecuredEndpointAndZioHandler[(String, Option[Json]), Unit]( + endpoints.postMaintenance, + (user: UserADM) => { case (action: String, jsonMaybe: Option[Json]) => + restService.executeMaintenanceAction(user, action, jsonMaybe) + } + ) + + val handlers = List(postMaintenanceHandler).map(mapper.mapEndpointAndHandler(_)) +} + +object MaintenanceEndpointsHandlers { + val layer = ZLayer.derive[MaintenanceEndpointsHandlers] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala index a895cb992e..f853ecf1ab 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/ProjectsEndpointsHandler.scala @@ -21,13 +21,13 @@ import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectS import org.knora.webapi.slice.admin.api.model.ProjectsEndpointsRequests.ProjectUpdateRequest import org.knora.webapi.slice.admin.api.service.ProjectADMRestService import org.knora.webapi.slice.common.api.EndpointAndZioHandler -import org.knora.webapi.slice.common.api.HandlerMapperF +import org.knora.webapi.slice.common.api.HandlerMapper import org.knora.webapi.slice.common.api.SecuredEndpointAndZioHandler final case class ProjectsEndpointsHandler( projectsEndpoints: ProjectsEndpoints, restService: ProjectADMRestService, - mapper: HandlerMapperF + mapper: HandlerMapper ) { val getAdminProjectsHandler = @@ -199,7 +199,7 @@ final case class ProjectsEndpointsHandler( ) } - val handlers = + private val handlers = List( getAdminProjectsHandler, getAdminProjectsKeywordsHandler, @@ -212,7 +212,7 @@ final case class ProjectsEndpointsHandler( getAdminProjectByProjectShortnameRestrictedViewSettingsHandler ).map(mapper.mapEndpointAndHandler(_)) - val secureHandlers = getAdminProjectsByIriAllDataHandler :: List( + private val secureHandlers = getAdminProjectsByIriAllDataHandler :: List( setAdminProjectsByProjectIriRestrictedViewSettingsHandler, setAdminProjectsByProjectShortcodeRestrictedViewSettingsHandler, getAdminProjectsByProjectIriMembersHandler, diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/MaintenanceRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/MaintenanceRestService.scala new file mode 100644 index 0000000000..b53b64431f --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/MaintenanceRestService.scala @@ -0,0 +1,56 @@ +/* + * 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.slice.admin.api.service + +import zio.IO +import zio.Task +import zio.ZIO +import zio.ZLayer +import zio.json.JsonDecoder +import zio.json.ast.Json + +import dsp.errors.BadRequestException +import org.knora.webapi.messages.admin.responder.usersmessages.UserADM +import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.ProjectsWithBakfilesReport +import org.knora.webapi.slice.admin.domain.service.MaintenanceService +import org.knora.webapi.slice.common.api.RestPermissionService + +final case class MaintenanceRestService( + securityService: RestPermissionService, + maintenanceService: MaintenanceService +) { + + private val fixTopLeftAction = "fix-top-left" + def executeMaintenanceAction(user: UserADM, action: String, jsonMaybe: Option[Json]): Task[Unit] = + securityService.ensureSystemAdmin(user) *> { + action match { + case `fixTopLeftAction` => executeTopLeftAction(jsonMaybe) + case _ => ZIO.fail(BadRequestException(s"Unknown action $action")) + } + } + + private def getParamsAs[A](paramsMaybe: Option[Json], actionName: String)(implicit + a: JsonDecoder[A] + ): IO[BadRequestException, A] = { + val missingArgsMsg = s"Missing arguments for $actionName" + def invalidArgsMsg(reason: String) = s"Invalid arguments for $actionName: $reason" + for { + json <- ZIO.fromOption(paramsMaybe).orElseFail(BadRequestException(missingArgsMsg)) + parsed = JsonDecoder[A].fromJsonAST(json) + result <- ZIO.fromEither(parsed).mapError(e => BadRequestException(invalidArgsMsg(e))) + } yield result + } + + private def executeTopLeftAction(topLeftParams: Option[Json]): IO[BadRequestException, Unit] = + for { + report <- getParamsAs[ProjectsWithBakfilesReport](topLeftParams, fixTopLeftAction) + _ <- maintenanceService.fixTopLeftDimensions(report).logError.forkDaemon + } yield () +} + +object MaintenanceRestService { + val layer = ZLayer.derive[MaintenanceRestService] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala index 95a004eb64..f9a6700225 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/ProjectsADMRestService.scala @@ -71,7 +71,7 @@ trait ProjectADMRestService { def updateProjectRestrictedViewSettings( id: ProjectIdentifierADM, user: UserADM, - payload: ProjectSetRestrictedViewSizeRequest + setSizeReq: ProjectSetRestrictedViewSizeRequest ): Task[ProjectRestrictedViewSizeResponseADM] } @@ -246,16 +246,16 @@ final case class ProjectsADMRestServiceLive( * * @param id the project's id represented by iri, shortcode or shortname, * @param user requesting user, - * @param payload value to be set, + * @param setSizeReq value to be set, * @return [[ProjectRestrictedViewSizeResponseADM]]. */ override def updateProjectRestrictedViewSettings( id: ProjectIdentifierADM, user: UserADM, - payload: ProjectSetRestrictedViewSizeRequest + setSizeReq: ProjectSetRestrictedViewSizeRequest ): Task[ProjectRestrictedViewSizeResponseADM] = for { - size <- ZIO.fromEither(RestrictedViewSize.make(payload.size)).mapError(BadRequestException(_)) + size <- ZIO.fromEither(RestrictedViewSize.make(setSizeReq.size)).mapError(BadRequestException(_)) project <- projectRepo.findById(id).someOrFail(NotFoundException(s"Project '${getId(id)}' not found.")) _ <- permissionService.ensureSystemOrProjectAdmin(user, project) _ <- projectRepo.setProjectRestrictedViewSize(project, size) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/MaintenanceService.scala b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/MaintenanceService.scala similarity index 64% rename from webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/MaintenanceService.scala rename to webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/MaintenanceService.scala index afecb5a00b..b795535607 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/admin/api/service/MaintenanceService.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/admin/domain/service/MaintenanceService.scala @@ -3,22 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.knora.webapi.slice.admin.api.service +package org.knora.webapi.slice.admin.domain.service +import zio.IO import zio.Task import zio.ZIO import zio.ZLayer import zio.macros.accessible import zio.stream.ZStream -import dsp.valueobjects.Project.Shortcode -import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.AssetId -import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.Dimensions -import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.ProjectsWithBakfilesReport -import org.knora.webapi.slice.admin.api.model.MaintenanceRequests.ReportAsset +import org.knora.webapi.slice.admin.api.model.MaintenanceRequests._ import org.knora.webapi.slice.admin.domain.model.KnoraProject -import org.knora.webapi.slice.admin.domain.service.KnoraProjectRepo -import org.knora.webapi.slice.admin.domain.service.ProjectADMService import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper import org.knora.webapi.slice.resourceinfo.domain.InternalIri import org.knora.webapi.store.triplestore.api.TriplestoreService @@ -28,8 +23,6 @@ import org.knora.webapi.store.triplestore.api.TriplestoreService.Queries.Update @accessible trait MaintenanceService { def fixTopLeftDimensions(report: ProjectsWithBakfilesReport): Task[Unit] - - def doNothing: Task[Unit] } final case class MaintenanceServiceLive( @@ -37,46 +30,63 @@ final case class MaintenanceServiceLive( triplestoreService: TriplestoreService, mapper: PredicateObjectMapper ) extends MaintenanceService { - override def doNothing: Task[Unit] = ZIO.unit - override def fixTopLeftDimensions(report: ProjectsWithBakfilesReport): Task[Unit] = - ZStream - .fromIterable(report.projects) - .flatMap { project => + + override def fixTopLeftDimensions(report: ProjectsWithBakfilesReport): Task[Unit] = { + def processProject(project: ProjectWithBakFiles): ZStream[Any, Throwable, Unit] = + getKnoraProject(project).flatMap { knoraProject => ZStream .fromIterable(project.assetIds) - .flatMapPar(5)(assetId => - ZStream.fromZIOOption( - fixAsset(project.id, assetId) - // None.type errors are just a sign that the assetId should be ignored. Some.type errors are real errors. - .tapSomeError { case Some(e) => ZIO.logError(s"Error while processing ${project.id}, $assetId: $e") } - // We have logged real errors above, from here on out ignore all errors so that the stream can continue. - .orElseFail(None) - ) - ) + .flatMapPar(5)(processSingleAsset(knoraProject, _)) } - .runDrain - private def fixAsset(shortcode: Shortcode, asset: ReportAsset): ZIO[Any, Option[Throwable], Unit] = + def getKnoraProject(project: ProjectWithBakFiles): ZStream[Any, Throwable, KnoraProject] = { + val getProjectZio: IO[Option[Throwable], KnoraProject] = projectRepo + .findByShortcode(project.id) + .some + .tapSomeError { case None => ZIO.logInfo(s"Project ${project.id} not found, skipping.") } + ZStream.fromZIOOption(getProjectZio) + } + + def processSingleAsset(knoraProject: KnoraProject, assetId: ReportAsset): ZStream[Any, Nothing, Unit] = + ZStream.fromZIOOption( + fixAsset(knoraProject, assetId) + // None.type errors are just a sign that the assetId should be ignored. Some.type errors are real errors. + .tapSomeError { case Some(e) => ZIO.logError(s"Error while processing ${knoraProject.id}, $assetId: $e") } + // We have logged real errors above, from here on out ignore all errors so that the stream can continue. + .orElseFail(None) + ) + + ZIO.logInfo(s"Starting fix top left maintenance") *> + ZStream.fromIterable(report.projects).flatMap(processProject).runDrain *> + ZIO.logInfo(s"Finished fix top left maintenance") + } + + private def fixAsset(project: KnoraProject, asset: ReportAsset): IO[Option[Throwable], Unit] = for { - project <- projectRepo.findByShortcode(shortcode).some + _ <- ZIO.logInfo(s"Checking asset $asset.") stillImageFileValueIri <- checkDimensions(project, asset) _ <- transposeImageDimensions(project, stillImageFileValueIri) + _ <- ZIO.logInfo(s"Transposed dimensions for asset $asset.") } yield () private def checkDimensions( project: KnoraProject, asset: ReportAsset - ): ZIO[Any, Option[Throwable], InternalIri] = + ): IO[Option[Throwable], InternalIri] = for { - res <- getDimensionAndStillImageValueIri(project, asset) - (dim, iri) = res - _ <- ZIO.when(dim == asset.dimensions)(ZIO.fail(None)) + result <- getDimensionAndStillImageValueIri(project, asset).tapSomeError { case None => + ZIO.logDebug(s"No StillImageFileValue with dimensions found for $asset, skipping.") + } + (actualDimensions, iri) = result + _ <- ZIO.when(actualDimensions == asset.dimensions)( + ZIO.logDebug(s"Dimensions for $asset already correct, skipping.") *> ZIO.fail(None) + ) } yield iri private def getDimensionAndStillImageValueIri( project: KnoraProject, asset: ReportAsset - ): ZIO[Any, Option[Throwable], (Dimensions, InternalIri)] = + ): IO[Option[Throwable], (Dimensions, InternalIri)] = for { result <- triplestoreService.query(checkDimensionsQuery(project, asset.id)).asSomeError rowMap <- ZIO.fromOption(result.results.bindings.headOption.map(_.rowMap)) @@ -107,7 +117,7 @@ final case class MaintenanceServiceLive( private def transposeImageDimensions( project: KnoraProject, stillImageFileValueIri: InternalIri - ): ZIO[Any, Option[Throwable], Unit] = + ): IO[Option[Throwable], Unit] = triplestoreService.query(transposeUpdate(project, stillImageFileValueIri)).asSomeError private def transposeUpdate(project: KnoraProject, stillImageFileValueIri: InternalIri) = { @@ -140,6 +150,5 @@ final case class MaintenanceServiceLive( } object MaintenanceServiceLive { - val layer = ZLayer.derive[MaintenanceServiceLive] } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala index f59b494e62..a3d1fb1bd8 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/HandlerMapper.scala @@ -42,7 +42,7 @@ case class SecuredEndpointAndZioHandler[INPUT, OUTPUT]( handler: UserADM => INPUT => Task[OUTPUT] ) -final case class HandlerMapperF()(implicit val r: zio.Runtime[Any]) { +final case class HandlerMapper()(implicit val r: zio.Runtime[Any]) { def mapEndpointAndHandler[INPUT, OUTPUT]( handlerAndEndpoint: SecuredEndpointAndZioHandler[INPUT, OUTPUT] @@ -58,6 +58,6 @@ final case class HandlerMapperF()(implicit val r: zio.Runtime[Any]) { UnsafeZioRun.runToFuture(zio.refineOrDie { case e: RequestRejectedException => e }.either) } -object HandlerMapperF { - val layer = ZLayer.fromZIO(ZIO.runtime[Any].map(HandlerMapperF()(_))) +object HandlerMapper { + val layer = ZLayer.fromZIO(ZIO.runtime[Any].map(HandlerMapper()(_))) } diff --git a/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala b/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala index e0d8574fcf..a9ffed83a6 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/common/api/TapirToPekkoInterpreter.scala @@ -15,13 +15,17 @@ import sttp.tapir.server.metrics.zio.ZioMetrics import sttp.tapir.server.model.ValuedEndpointOutput import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter import sttp.tapir.server.pekkohttp.PekkoHttpServerOptions +import zio.ZLayer import zio.json.DeriveJsonCodec import zio.json.JsonCodec import scala.concurrent.ExecutionContext import scala.concurrent.Future -final case class TapirToPekkoInterpreter()(implicit executionContext: ExecutionContext) { +import org.knora.webapi.core.ActorSystem + +final case class TapirToPekkoInterpreter()(actorSystem: ActorSystem) { + implicit val executionContext: ExecutionContext = actorSystem.system.dispatcher private case class GenericErrorResponse(error: String) private object GenericErrorResponse { implicit val codec: JsonCodec[GenericErrorResponse] = DeriveJsonCodec.gen[GenericErrorResponse] @@ -41,3 +45,7 @@ final case class TapirToPekkoInterpreter()(implicit executionContext: ExecutionC def toRoute(endpoint: ServerEndpoint[PekkoStreams with WebSockets, Future]): Route = interpreter.toRoute(endpoint) } + +object TapirToPekkoInterpreter { + val layer = ZLayer.derive[TapirToPekkoInterpreter] +} diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala index e6202344a6..4e729a7aff 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectADMRestServiceMock.scala @@ -110,7 +110,7 @@ object ProjectADMRestServiceMock extends Mock[ProjectADMRestService] { override def updateProjectRestrictedViewSettings( id: ProjectIdentifierADM, user: UserADM, - size: ProjectSetRestrictedViewSizeRequest + setSizeReq: ProjectSetRestrictedViewSizeRequest ): Task[ProjectRestrictedViewSizeResponseADM] = ??? } } diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala index 6abae540ce..2ec8470cfe 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMMock.scala @@ -75,18 +75,18 @@ object ProjectsResponderADMMock extends Mock[ProjectsResponderADM] { ): Task[ProjectRestrictedViewSettingsGetResponseADM] = proxy(ProjectRestrictedViewSettingsGetRequestADM, id) override def projectCreateRequestADM( - createPayload: ProjectCreateRequest, + createReq: ProjectCreateRequest, requestingUser: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = - proxy(ProjectCreateRequestADM, (createPayload, requestingUser, apiRequestID)) + proxy(ProjectCreateRequestADM, (createReq, requestingUser, apiRequestID)) override def changeBasicInformationRequestADM( projectIri: Iri.ProjectIri, - updatePayload: ProjectUpdateRequest, + updateReq: ProjectUpdateRequest, user: UserADM, apiRequestID: UUID ): Task[ProjectOperationResponseADM] = - proxy(ChangeBasicInformationRequestADM, (projectIri, updatePayload, user, apiRequestID)) + proxy(ChangeBasicInformationRequestADM, (projectIri, updateReq, user, apiRequestID)) } } } diff --git a/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/MaintenanceServiceLiveSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/MaintenanceServiceLiveSpec.scala index 524021fdf8..fce80a4062 100644 --- a/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/MaintenanceServiceLiveSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/slice/admin/api/service/MaintenanceServiceLiveSpec.scala @@ -16,6 +16,8 @@ import org.knora.webapi.TestDataFactory import org.knora.webapi.messages.StringFormatter import org.knora.webapi.slice.admin.api.model.MaintenanceRequests._ import org.knora.webapi.slice.admin.domain.repo.service.KnoraProjectRepoInMemory +import org.knora.webapi.slice.admin.domain.service.MaintenanceService +import org.knora.webapi.slice.admin.domain.service.MaintenanceServiceLive import org.knora.webapi.slice.admin.domain.service.ProjectADMService import org.knora.webapi.slice.common.repo.service.PredicateObjectMapper import org.knora.webapi.slice.resourceinfo.domain.IriConverter @@ -29,6 +31,7 @@ import org.knora.webapi.store.triplestore.api.TriplestoreServiceInMemory.emptyDa object MaintenanceServiceLiveSpec extends ZIOSpecDefault { private val testProject = TestDataFactory.someProject + private val createProject = ZIO.serviceWithZIO[KnoraProjectRepoInMemory](_.save(testProject)) private val projectDataNamedGraphIri = ProjectADMService.projectDataNamedGraphV2(testProject).value private val testAssetId = AssetId.unsafeFrom("some-asset-id") private val expectedDimension = Dimensions(5202, 3602) @@ -73,15 +76,21 @@ object MaintenanceServiceLiveSpec extends ZIOSpecDefault { val spec = suite("MaintenanceServiceLive")( test("fixTopLeftDimensions should not fail for an empty report") { - MaintenanceService.fixTopLeftDimensions(ProjectsWithBakfilesReport(Chunk.empty)).as(assertCompletes) + createProject *> + saveStillImageFileValueWithDimensions(width = expectedDimension.height, height = expectedDimension.width) *> + MaintenanceService.fixTopLeftDimensions(ProjectsWithBakfilesReport(Chunk.empty)).as(assertCompletes) }, test("fixTopLeftDimensions should not fail if no StillImageFileValue is found") { + createProject *> + MaintenanceService.fixTopLeftDimensions(testReport).as(assertCompletes) + }, + test("fixTopLeftDimensions should not fail if project is not found") { MaintenanceService.fixTopLeftDimensions(testReport).as(assertCompletes) }, test("fixTopLeftDimensions should transpose dimension for an existing StillImageFileValue") { for { // given - _ <- ZIO.serviceWithZIO[KnoraProjectRepoInMemory](_.save(testProject)) + _ <- createProject _ <- saveStillImageFileValueWithDimensions(width = expectedDimension.height, height = expectedDimension.width) // when _ <- MaintenanceService.fixTopLeftDimensions(testReport) @@ -89,10 +98,12 @@ object MaintenanceServiceLiveSpec extends ZIOSpecDefault { actualDimension <- queryForDim() } yield assertTrue(actualDimension == expectedDimension) }, - test("fixTopLeftDimensions not should transpose dimension for an existing StillImageFileValue") { + test( + "fixTopLeftDimensions should not transpose dimension for an existing StillImageFileValue if the dimensions are correct" + ) { for { // given - _ <- ZIO.serviceWithZIO[KnoraProjectRepoInMemory](_.save(testProject)) + _ <- createProject _ <- saveStillImageFileValueWithDimensions(width = expectedDimension.width, height = expectedDimension.height) // when _ <- MaintenanceService.fixTopLeftDimensions(testReport)