All Downloads are FREE. Search and download functionalities are using the official Maven repository.

golden.framework.web.JavalinApplicationBuilder.scala Maven / Gradle / Ivy

package golden.framework.web

import golden.framework.bind.{Container, ContainerBuilder, Module}
import golden.framework.json.JsonMapper
import golden.framework.{Environment, StartupTask, Type}
import io.javalin.Javalin
import io.javalin.http.staticfiles.Location
import io.javalin.http.{Handler, HandlerType}
import io.javalin.util.JavalinLogger
import golden.framework.OptionExtensions.tap
import io.javalin.json.JsonMapper as JavalinJsonMapper
import java.io.{ByteArrayInputStream, InputStream}
import java.lang.reflect.Type as JType
import java.nio.charset.StandardCharsets.UTF_8
import scala.collection.mutable.{ArrayBuffer as MutableArray, HashMap as MutableMap}

private class JavalinApplicationBuilder extends ApplicationBuilder:

  private var _scopedMethods = Seq("POST", "PUT", "PATCH", "DELETE", "GET")
  private val _modules = MutableArray.empty[Module]
  private val _handlerTypes = MutableMap.empty[Type, Iterable[HttpMethodAnnotation]]
  private val _handlers = MutableArray.empty[(String, RequestHandler, Iterable[HandlerType])]
  private var _commandLineArgs = Seq.empty[String]
  private var _logRequests: Option[Boolean] = None
  private var _enableLogging = true

  def registerModule(module: Module): ApplicationBuilder = {
    _modules += module
    this
  }

  def requestLogging(enable: Boolean): ApplicationBuilder = {
    _logRequests = Some(enable)
    this
  }

  def logging(enable: Boolean): ApplicationBuilder = {
    _enableLogging = enable
    this
  }

  def addHandlerType(handlerType: Type, methods: Iterable[HttpMethodAnnotation]): ApplicationBuilder = {
    _handlerTypes.update(handlerType, methods)
    this
  }

  def addHandler(method: String, path: String, handler: RequestHandler): ApplicationBuilder = {
    _handlers += ((path, handler, method.toUpperCase.split(',').map(HandlerType.valueOf)))
    this
  }

  def setCommandLineArgs(args: Seq[String]): ApplicationBuilder = {
    _commandLineArgs = args
    this
  }

  def scopedRequestTypes(methods: String*): ApplicationBuilder = {
    _scopedMethods = methods.toSeq
    this
  }

  def build(): Application =
    buildJavalinApplication()

  private def buildJavalinApplication(): Application = {
    JavalinLogger.startupInfo = false
    JavalinLogger.enabled = _enableLogging

    val container = createContainer()

    val startupTasks = container.get[List[StartupTask]].sortBy(_.order)
    startupTasks.filter(_.beforeStart).foreach(_.execute())

    val environment = container.get[WebEnvironment]
    val jsonMapper = container.get[JsonMapper]
    val javalinApp = Javalin.create { config =>

      config.showJavalinBanner = false

      config.jsonMapper(createJsonMapper(jsonMapper))

      if _logRequests.getOrElse(environment.isDevelopment) then
        config.plugins.enableDevLogging()

      if environment.isDevelopment then
        config.plugins.enableCors(cors => cors.add(_.anyHost()))

      if environment.webRootDir.nonEmpty then
        config.staticFiles.add(environment.webRootDir.get, Location.EXTERNAL)
        if environment.isSPAFront.getOrElse(false) then
          config.spaRoot.addFile("/", "index.html", Location.EXTERNAL)
    }

    val app = createJavalinApplication(javalinApp, container)
    container.get[ApplicationWrapper].set(app)

    startupTasks.filterNot(_.beforeStart).foreach(_.execute())

    configureRequestScopeStart(javalinApp, container)
    registerHandlers(javalinApp)
    configureRequestScopeEnd(javalinApp)

    app
  }

  private def createContainer(): Container = {
    val builder = ContainerBuilder.create()
    _modules.foreach(_.load(builder))
    registerCommonServices(builder, _commandLineArgs)
    builder.build()
  }

  private def registerCommonServices(builder: ContainerBuilder, commandLineArgs: Seq[String]): Unit = {
    val _commandLineArgs = commandLineArgs
    val defaultEnvironment = new WebEnvironment {
      val isDevelopment: Boolean = true
      val isProduction: Boolean = false
      val commandLineArgs: Seq[String] = _commandLineArgs
      val currentDir: String = sys.props.get("user.dir").get
      val webRootDir: Option[String] = None
      val isSPAFront: Option[Boolean] = None
    }

    builder.registerInstance(defaultEnvironment).as[WebEnvironment]().as[Environment]().asSingleton()
    builder.registerInstance(new JsonMapper()).as[JsonMapper]().asSingleton()
    builder.registerInstance(new ApplicationWrapper()).as[ApplicationWrapper]().asSingleton()
    builder.register(container => container.tags.collectFirst { case context: HttpContext => context }.get)
      .as[HttpContext]()
      .asContainerScoped()
  }

  private def createJsonMapper(jsonMapper: JsonMapper): JavalinJsonMapper = {
    new JavalinJsonMapper {

      override def toJsonString(obj: Any, tpe: JType): String =
        jsonMapper.serialize(obj)

      override def toJsonStream(obj: Any, tpe: JType): InputStream =
        ByteArrayInputStream(toJsonString(obj, tpe).getBytes(UTF_8))

      override def fromJsonString[T](json: String, tpe: JType): T =
        jsonMapper.deserialize[T](json, tpe)

      override def fromJsonStream[T](json: InputStream, tpe: JType): T =
        jsonMapper.deserialize[T](json, tpe)
    }
  }

  private def createJavalinApplication(app: Javalin, container: Container): Application = {
    new Application {

      def start(host: Option[String], port: Option[Int]): Application = {
        if host.nonEmpty then app.start(host.get, port.get)
        else if port.nonEmpty then app.start(port.get)
        else app.start()
        this
      }

      def stop(): Application = {
        container.close()
        app.stop()
        sys.exit()
        this
      }
    }
  }

  private def configureRequestScopeStart(app: Javalin, container: Container): Unit = {
    app.before { context =>
      if _scopedMethods.exists(_.equalsIgnoreCase(context.method.name)) then {
        val httpContext = JavalinHttpContext(context)
        val scope = container.createScope(httpContext)
        httpContext.attribute("scope", scope)
        httpContext.attribute("httpContext", httpContext)
      }
    }
  }

  private def configureRequestScopeEnd(app: Javalin): Unit = {
    app.after { context =>
      if _scopedMethods.exists(_.equalsIgnoreCase(context.method.name)) then {
        val scope = Option(context.attribute[Container]("scope"))
        scope.tap(_.close())
      }
    }
  }

  private def registerHandlers(app: Javalin): Unit = {

    _handlers.toSeq
      .flatMap { case (path, handler, handlerTypes) => handlerTypes.map((path, handler) -> _) }
      .foreach { case ((path, handler), handlerType) =>
        val javalinHandler: Handler = { context =>
          val httpContext = context.attribute[HttpContext]("httpContext")
          handler.handle(httpContext)
        }
        app.addHandler(handlerType, path, javalinHandler)
      }

    _handlerTypes.toSeq
      .flatMap { case (handlerType, methods) => methods.map(handlerType -> _) }
      .sortBy { case (_, method) => method.order }
      .foreach { case (reqHandlerType, httpMethod) =>
        val handlerType = toJavalinHandlerType(httpMethod)
        val handler = createJavalinHandler(reqHandlerType)
        app.addHandler(handlerType, httpMethod.path, handler)
      }

    def createJavalinHandler(handlerType: Type): Handler = { context =>
      val httpContext = context.attribute[HttpContext]("httpContext")
      val scope = context.attribute[Container]("scope")
      val handler = scope.get(handlerType).asInstanceOf[RequestHandler]
      handler.handle(httpContext)
    }
  }

  private def toJavalinHandlerType(httpMethod: HttpMethodAnnotation): HandlerType = httpMethod match
    case _: httpGet => HandlerType.GET
    case _: httpPost => HandlerType.POST
    case _: httpPut => HandlerType.PUT
    case _: httpPatch => HandlerType.PATCH
    case _: httpDelete => HandlerType.DELETE
    case _ => throw IllegalArgumentException(s"invalid http method: ${httpMethod.getClass}")

  private class ApplicationWrapper:
    private var _app: Option[Application] = None

    def get: Application = _app.getOrElse {
      throw IllegalStateException("application not started yet")
    }

    def set(app: Application): Unit = _app = Some(app)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy