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

io.bluebank.braid.server.JsonRPCVerticle.kt Maven / Gradle / Ivy

There is a newer version: 4.1.2-RC13
Show newest version
/**
 * Copyright 2018 Royal Bank of Scotland
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.bluebank.braid.server

import io.bluebank.braid.core.http.setupAllowAnyCORS
import io.bluebank.braid.core.http.setupOptionsMethod
import io.bluebank.braid.core.http.write
import io.bluebank.braid.core.jsonrpc.JsonRPCMounter
import io.bluebank.braid.core.jsonrpc.JsonRPCRequest
import io.bluebank.braid.core.jsonrpc.JsonRPCResponse
import io.bluebank.braid.core.logging.loggerFor
import io.bluebank.braid.core.meta.ServiceDescriptor
import io.bluebank.braid.core.meta.defaultServiceEndpoint
import io.bluebank.braid.core.reflection.serviceName
import io.bluebank.braid.core.security.AuthenticatedSocket
import io.bluebank.braid.core.service.ConcreteServiceExecutor
import io.bluebank.braid.core.service.ServiceExecutor
import io.bluebank.braid.core.socket.SockJSSocketWrapper
import io.bluebank.braid.core.socket.TypedSocket
import io.bluebank.braid.server.services.CompositeExecutor
import io.bluebank.braid.server.services.JavascriptExecutor
import io.vertx.core.AbstractVerticle
import io.vertx.core.Future
import io.vertx.core.http.HttpHeaders
import io.vertx.core.http.HttpServerOptions
import io.vertx.ext.auth.AuthProvider
import io.vertx.ext.web.Router
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.ext.web.handler.StaticHandler
import io.vertx.ext.web.handler.sockjs.SockJSHandler
import io.vertx.ext.web.handler.sockjs.SockJSSocket

class JsonRPCVerticle(
  private val rootPath: String, val services: List, val port: Int,
  private val authProvider: AuthProvider?,
  private val httpServerOptions: HttpServerOptions
) : AbstractVerticle() {

  companion object {
    val logger = loggerFor()
  }

  val serviceMap: MutableMap by lazy {
    val serviceNames = services.map { getServiceName(it) }
    val jsOnlyServices =
      JavascriptExecutor.queryServiceNames(vertx).filter { !serviceNames.contains(it) }
    val mutableServiceMap = mutableMapOf()
    services.map { getServiceName(it) to wrapConcreteService(it) }.forEach {
      mutableServiceMap[it.first] = it.second
    }
    jsOnlyServices.map { it to JavascriptExecutor(vertx, it) }.forEach {
      mutableServiceMap[it.first] = it.second
    }
    mutableServiceMap
  }

  private lateinit var router: Router
  private lateinit var sockJSHandler: SockJSHandler

  override fun start(startFuture: Future) {
    router = setupRouter()
    setupWebserver(router, startFuture)
  }

  private fun wrapConcreteService(service: Any): ServiceExecutor {
    return CompositeExecutor(
      JavascriptExecutor(vertx, getServiceName(service)),
      ConcreteServiceExecutor(service)
    )
  }

  private fun ServiceExecutor.getJavascriptExecutor(): JavascriptExecutor {
    return when (this) {
      is CompositeExecutor -> {
        executors
          .filter { it is JavascriptExecutor }
          .map { it as JavascriptExecutor }
          .firstOrNull() ?: throw RuntimeException("cannot find javascript executor")
      }
      is JavascriptExecutor -> this
      else -> throw RuntimeException("found executor is not a ${JavascriptExecutor::class.simpleName} or doesn't contain one")
    }
  }

  private fun ServiceExecutor.getConcreteExecutor(): ConcreteServiceExecutor? {
    return when (this) {
      is CompositeExecutor -> {
        executors.filter { it is ConcreteServiceExecutor }
          .map { it as ConcreteServiceExecutor }
          .firstOrNull()
      }
      is ConcreteServiceExecutor -> this
      else -> null
    }
  }

  private fun getJavascriptExecutorForService(serviceName: String): JavascriptExecutor {
    return serviceMap.computeIfAbsent(serviceName) {
      bindServiceSockJSHandler(serviceName)
      JavascriptExecutor(vertx, serviceName)
    }.getJavascriptExecutor()
  }

  private fun getJavaExecutorForService(serviceName: String): ConcreteServiceExecutor? {
    val service = serviceMap[serviceName] ?: return null
    return service.getConcreteExecutor()
  }

  private fun setupWebserver(router: Router, startFuture: Future) {
    vertx.createHttpServer(httpServerOptions.withCompatibleWebsockets())
      .requestHandler(router::accept)
      .listen(port) {
        if (it.succeeded()) {
          logger.info("started on port $port")
          startFuture.complete()
        } else {
          logger.error("failed to start because", it.cause())
          startFuture.fail(it.cause())
        }
      }
  }

  private fun HttpServerOptions.withCompatibleWebsockets(): HttpServerOptions {
    this.websocketSubProtocols = "undefined"
    return this
  }

  private val servicesRouter: Router = Router.router(vertx)

  private fun setupRouter(): Router {
    val router = Router.router(vertx)
    router.setupAllowAnyCORS()
    router.setupOptionsMethod()
    setupSockJS()
    servicesRouter.post().handler(BodyHandler.create())
    servicesRouter.get("/").handler { it.getServiceList() }
    servicesRouter.get("/:serviceId").handler { it.getService(it.pathParam("serviceId")) }
    servicesRouter.get("/:serviceId/script")
      .handler { it.getServiceScript(it.pathParam("serviceId")) }
    servicesRouter.post("/:serviceId/script")
      .handler { it.saveServiceScript(it.pathParam("serviceId"), it.bodyAsString) }
    servicesRouter.delete("/:serviceId")
      .handler { it.deleteService(it.pathParam("serviceId")) }
    servicesRouter.get("/:serviceId/java")
      .handler { it.getJavaImplementationHeaders(it.pathParam("serviceId")) }
    router.mountSubRouter(rootPath, servicesRouter)
    router.get()
      .last()
      .handler(
        StaticHandler.create("editor-web", JsonRPCVerticle::class.java.classLoader)
          .setCachingEnabled(false)
          .setMaxCacheSize(1)
          .setCacheEntryTimeout(1)
      )
    return router
  }

  private fun RoutingContext.getServiceList() {
    val sm = ServiceDescriptor.createServiceDescriptors(rootPath, serviceMap.keys)
    write(sm)
  }

  private fun RoutingContext.getService(serviceName: String) {
    data class ServiceDocumentation(
      val java: String,
      val script: String,
      val endpoint: String
    )

    val docs = ServiceDocumentation(
      "$rootPath$serviceName/java",
      "$rootPath$serviceName/script",
      defaultServiceEndpoint(rootPath, serviceName)
    )
    write(docs)
  }

  private fun RoutingContext.getServiceScript(serviceName: String) {
    val script = getJavascriptExecutorForService(serviceName).getScript()

    response()
      .putHeader(HttpHeaders.CONTENT_TYPE, "text/javascript")
      .putHeader(HttpHeaders.CONTENT_LENGTH, script.length().toString())
      .end(script)
  }

  private fun RoutingContext.deleteService(serviceName: String) {
    val service = serviceMap[serviceName]
    if (service == null) {
      response().setStatusMessage("no service called $serviceName").end()
      return
    }
    getJavascriptExecutorForService(serviceName).deleteScript()
    if (service is CompositeExecutor) {
      response()
        .setStatusMessage("cannot delete java service $serviceName, but have deleted JS extension script")
        .end()
      return
    }
    serviceMap.remove(serviceName)
    val searchPath = sockPath(serviceName).dropLast(1) // remove the *
    router.routes.filter { it.path == searchPath }.forEach {
      logger.info("remove route $it")
      it.remove()
    }
    write("done")
  }

  private fun sockPath(serviceName: String) = "$rootPath$serviceName/*"

  private fun RoutingContext.getJavaImplementationHeaders(serviceName: String) {
    val service = getJavaExecutorForService(serviceName)

    if (service == null) {
      write("")
      return
    } else {
      write(service.getStubs())
    }
  }

  private fun RoutingContext.saveServiceScript(serviceName: String, script: String) {
    val service = getJavascriptExecutorForService(serviceName)
    try {
      service.updateScript(script)
      response().end()
    } catch (err: Throwable) {
      write(err)
    }
  }

  private fun socketHandler(socket: SockJSSocket) {
    val re = Regex("${rootPath.replace("/", "\\/")}([^\\/]+).*")
    val serviceName = re.matchEntire(socket.uri())?.groupValues?.get(1) ?: ""

    val service = serviceMap[serviceName]
    if (service != null) {
      // TODO: the pipeline setup is complex. rework this to ease comprehension
      // the slight gotcha is that the service may or may not be authenticated
      // perhaps all services should be authenticated?
      val sockWrapper = with(SockJSSocketWrapper.create(socket, vertx)) {
        if (authProvider != null) {
          val authenticatedSocket = AuthenticatedSocket.create(authProvider)
          this.addListener(authenticatedSocket)
          authenticatedSocket
        } else {
          this
        }
      }

      val rpcSocket = TypedSocket.create()
      sockWrapper.addListener(rpcSocket)
      val mount = JsonRPCMounter(service, vertx)
      rpcSocket.addListener(mount)
    } else {
      socket.write("cannot find service $service")
      socket.close()
    }
  }

  private fun getServiceName(service: Any): String {
    return service.javaClass.serviceName()
  }

  private fun setupSockJS() {
    sockJSHandler = SockJSHandler.create(vertx)
    sockJSHandler.socketHandler(this::socketHandler)
    // mount each service

    servicesRouter.get("/:serviceId/braid/info").handler {
      val serviceId = it.pathParam("serviceId")
      if (serviceMap.contains(serviceId)) {
        it.next()
      } else {
        it.response()
          .setStatusMessage("""Braid: Service '$serviceId' does not exist. Click here to create it http://localhost:8080""".trimMargin())
          .setStatusCode(404)
          .end()
      }
    }
    serviceMap.keys.forEach {
      bindServiceSockJSHandler(it)
    }
  }

  private fun bindServiceSockJSHandler(serviceName: String) {
    servicesRouter.route("/$serviceName/braid/*").handler(sockJSHandler)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy