style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Kotlin, Undertow como proxy

Introduccion

Las tecnologias del front están creciendo como la espuma y bien sabemos que el front ya no es solamente hacer validaciones o hacer marquesinas, ahora se desarrollan aplicaciones enteras con el concepto de Single Page Applications, dejando a Java solo en el Middleware o en Android. Y después de Kotlin ya ni en Android :P. Seamos sinceros al comparar una aplicación hecha con React o con Angular contra una aplicación en JSF, se nota el porque casi todas las grandes compañías del mundo voltearon a ver a Nodejs (que es mucho muy bueno) y a tecnologías puramente de Front.

Dejando atrás las discusiones sobre si Tomcat o Spring Batch o EJB o Nodejs, que ademas de nunca acabar, no dejan de ser servidores que entregan archivos HTML, JS, CSS y otros (pdf, imagenes, txt, csv, xls, xml), que pueden estar hechos como tu quieras (React, Angular, Aurelia, Vue, Jquery). Por lo que un servidor http siempre esta involucrado; muchas soluciones actuales dejan totalmente el Front en Nodejs y los servicios en Java, otros combinan Nginx con Nodejs y Java; mientras otros solo Java o solo Nodejs.

Las razones porque elegir uno u otro, pues dependerán de quien este tomando esas decisiones, en nuestro pequeño caso, vamos a crear un proxy http para servir contenido estático, de momento es un simple HTML un simple CSS y un simple JS.

Ademas, Camel esta separado de JAX-RS en las validaciones por lo que los errores no entraran en las rutas, puedes corroborar esto tratando de insertar una Question con algún parámetro invalido, la respuesta la obtendrás de CFX no de Camel, por tanto si quisiéramos hacer algo mas con ese error (bitacorizar, aplicar AI, enviar un SMS, un correo o avisarle al dueño de la empresa y a todos los vecinos); pues forzosamente necesitamos algo mas que lo haga, un proxy y si, lo podemos hacer con Camel para aplicar todo lo aprendido para integrarlo a cualquier endpoint.

Pre-requisitos

Desarrollo

Dependencias

Si haz trabajado con Wildfly o con JBoss EAP, en sus ultimas versiones, sabras que Undertow es uno de los elementos que los hacen realmente veloces al momento de servir applicaciones Web y servicios Web, Undertow es un servidor web hecho en java sumamente rápido, por eso lo vamos usar como nuestro proxy.

Agregamos a nuestro archivo build.gradle

compile "org.apache.camel:camel-undertow:$camel_version"

Mejorando la respuesta al validar Question

Si hiciste la prueba, cuando tratas de guardar una Question con nombres de campo no validos (correctAnafre en lugar de correctAnswer por ejemplo). Notaras que el servicio regresa un nada agradable 500, ya mencione que esto se debe a la separación que existe entre JAX-RS y Camel, por eso vamos a agregar un handler para errores de este tipo de este modo podemos enviar una respuesta mas adecuada a nuestros clientes (clientes del API).

Creamos un nuevo Kotlin File/Class en la carpeta Java de nombre InvalidBodyProvider:

import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import org.slf4j.LoggerFactory
import javax.ws.rs.core.Response
import javax.ws.rs.ext.ExceptionMapper

@javax.ws.rs.ext.Provider
class InvalidBodyProvider: ExceptionMapper<UnrecognizedPropertyException> {

    private val log = LoggerFactory.getLogger(InvalidBodyProvider::class.java)

    override fun toResponse(exception: UnrecognizedPropertyException): Response {
        var message = "Los parametros de entrada no son validos, por favor verifica tu peticion"
        log.info(message)
        return Response.status(Response.Status.BAD_REQUEST).entity(message).build()
    }
}

Ahora si cuando volvamos a intentar a enviar la Question con un mal cuerpo recibiremos un 400 con el mensaje adecuado. Pero esta validacion es 100% JAX-RS, esta respuesta no entrara a la Ruta de Camel y no podemos hacer nada en QuestionRoute si quisiéramos hacer algo con los errores 400.

Agregando mas properties

En el archivo camel.properties agregamos los parámetros para el nuevo servidor

staticPort=8080
staticHost=0.0.0.0

Creando el proxy http

Creamos un archivo Kotlin de nombre StaticServer en la carpeta java:

import org.apache.camel.Message
import org.apache.camel.builder.RouteBuilder
import javax.ws.rs.core.MediaType

enum class FileTypes(val value: String) {
    HTML("html"),
    JS("js"),
    CSS("css")
}

class StaticServer: RouteBuilder() {

    private fun getMessageOut(fileExtension: String, file: String, messageOut: Message) = {
        messageOut.setHeader("ContentType", mimeType(fileExtension))
        messageOut.body = staticFile(file)
        messageOut
    }

    private fun mimeType(fileExtension: String =  "/index.html") : String {
        return when (fileExtension) {
            FileTypes.HTML.value -> MediaType.TEXT_HTML
            FileTypes.JS.value -> "application/javascript"
            FileTypes.CSS.value -> "text/css"
            else -> MediaType.TEXT_PLAIN
        }
    }

    private fun staticFile(file: String =  "/index.html") : String {
        val fileName = "webapp$file"
        return StaticServer::class.java.classLoader.getResource(fileName).readText()
    }

    override fun configure() {
        from("undertow:http://{{staticHost}}:{{staticPort}}/wizard")
            .choice()
                .`when`(header("CamelHttpPath").startsWith("/question"))
                    .to("undertow:http://{{host}}:{{port}}/wizard/question?enableOptions=true")
                .endChoice()
                .otherwise()
                    .process({ exchange ->
                        val file = exchange.`in`.getHeader("CamelHttpPath").toString()
                        val fileExtension = file.substringAfter(".")
                        val fileGetter = getMessageOut(fileExtension, file, exchange.out)
                        exchange.out = fileGetter()
                    })
                .endChoice()
            .end()

    }
}

Este es el Servidor de archivos estáticos y proxy http para nuestros servicios, al instalar esto en docker o en un servidor el único puerto que debemos habilitar es 8080 para el caso de que se use el mismo numero que yo puse de configuración. Solamente hacemos uso del choice para saber si el Path contiene question, o si se esta pidiendo un archivo.

A partir de esta linea .to("undertow:http://{{host}}:{{port}}/wizard/question?enableOptions=true") podemos realizar todos los procesos adicionales que queramos con la respuesta del servicio Question, ya sea exitosa o no.

Agregando contenido estatico

En la carpeta de resources, agregue una carpeta para el contenido web llamada web app con la siguiente estructura:

index.html
css
|==styles.css
js
|==app.js

El contenido no es tan importante de momento, lo puedes obtener del repo git que estará en los comentarios finales

Actualizando el Main

Finalmente actualizaremos nuestro archivo Main para agregar la ruta y el Provider

import org.apache.camel.CamelContext
import org.apache.camel.impl.DefaultCamelContext

import com.betotto.question.QuestionRoute
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider

import org.apache.camel.component.properties.PropertiesComponent
import java.util.Arrays
import org.apache.camel.impl.JndiRegistry

fun main(args: Array<String>) {
    val registry = JndiRegistry()
    registry.bind("restJacksonProviderList", Arrays.asList<Any>(JacksonJsonProvider(),
            InvalidBodyProvider()))
    val context: CamelContext = DefaultCamelContext(registry)

    val props = PropertiesComponent()
    props.setLocation("classpath:camel.properties")
    context.addComponent("properties", props)

    context.addRoutes(CxfrServer())
    context.addRoutes(StaticServer())
    context.addRoutes(QuestionRoute())

    context.start()
}

Resultado final

Al final podremos ver que todo funciona de maravilla, la salida variara dependiendo que tengas en la base.

Notas finales

Este es el commit de este post recuerda no hacer caso de los archivos de la carpeta idea, son necesarios solo para el IDE

Los archivos de la carpeta web app los puedes obtener de ahi.

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">