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

Kotlin, Camel y CXF

Mock del primer servicio

Una vez que tenemos la base del proyecto, podemos agregar nuestro primer servicio REST usaremos la referencia JAX-RS 2 con la implementacion Apache CXF.

Nuestro primer servicio sera un endpoint de preguntas, con dos operaciones la primera consultar una lista de preguntas, de momento escritas en el mismo codigo, y la segunda de esa lista de preguntas obtener una en particular por su codigo.

Prerequisitos

De momento el unico requerimiento previo es tener nuestro proyecto como lo dejamos en el primer post.

Mensajes de Camel

Apache camel al igual que EJB estan basados en los principios de lo que ahora conocemos como microservicios antes de que incluso se llamaran asi, pequenias aplicaciones que al sumarse todas dan como resultado un proceso complejo, cada microservicio se comunica con otros para resolver la tarea en especifica que tiene asignada.

Pues bien en una ruta interactuan diversos procesos, dependiendo del contenido de los datos se toman decisiones de flujo y se consume o envia informacion a diferentes endpoints. La forma en que Camel comparte informacion entre los diferentes endpoints, protocolos y procesos, es mediante el uso de Mensajes, que son muy parecidos a los mensajes HTTP tienen un header y un body, el body es el contenido mismo del mensaje, mientras que el header es meta-data que sirve para manipular o modificar de algun modo el body.

En todo punto de decision de camel existe un mensaje de entrada, que es el que se recibe y un mensaje de salida que es el que se envia al siguiente punto del proceso, si estamos en un endpoint de entrada el mensaje de entrada se genera con base en el protocolo definido (HTTP por el momento) y si es un endpoint de salida el mensaje se transfiere con el protocolo adecuado.

Desarrollo

Configurando dependencias

El primer paso es actualizar nuestras dependencias en el archivo build.gradle:

  • compile "org.apache.camel:camel-cxf:$camel_version"
  • compile group: 'org.apache.cxf', name: 'cxf-rt-transports-http-jetty', version: '3.2.1'
  • compile group: 'com.google.code.gson', name: 'gson', version: '2.8.2'

Definiendo el API del servicio

A mi en lo personal me gusta mas trabajar con paquetes por modulo en lugar de paquetes por tipo de componente, sobre todo en mantenimiento, por lo que vamos a crear el paquete com.betotto.question para ello hay que dar clic secundario en el folder src/main/Java -> new Package.

Este paquete es ahora el modulo question, en este modulo vamos a crear un nuevo Kotlin File/Class, dando clic secundario en el paquete que acabamos de crear, le ponemos por nombre QuestionApi con el siguiente contenido:

package com.betotto.question

import javax.ws.rs.GET
import javax.ws.rs.PathParam
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response

@Path("/")
interface QuestionApi {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    fun getAllQuestions(): Response

    @GET
    @Path("/{idQuestion}")
    @Produces(MediaType.APPLICATION_JSON)
    fun getQuestionById(@PathParam("idQuestion") idQuestion: Int): Response
}

Estamos creando una interfaz que define un Resource en el estilo de JAX-RS, basicamente tenemos dos operaciones GET una en el path "/" y otra en el path "/idQuestion" idQuestion es el id de la pregunta que vamos a consultar, ambas producen JSON como respuesta, en Kotlin los metodos se llaman funciones, por lo que ambas funciones regresan un tipo Response, podriamos dejar los objetos como respuesta en lugar de Response y dejar que Gson los convierta automaticamente a JSON, pero a mi me gusta usar Response, mas adelante les explico porque.

Definiendo la clase de datos

Lo que sigue es definir una clase que contenga los campos que describen una Question (voy a spanglear un poco con los nombres de clases y variables), con sus correspondientes getters y setters, con un constructor para que solamente podamos crear Questions con Id, con el texto de la pregunta inicializado en cadena vacia y que el Id no se pueda modificar una vez creado el objeto, para ello creamos un nuevo Kotlin File/Class en el modulo question (paquete com.betotto.question) de nombre Question con el contenido:

package com.betotto.question

data class Question(val id: Int, var question: String = "")

Si, a mi tambien me gusto mucho esto. los getters y setter son por defecto en Kotlin val no se puede modificar (es immutable) y el constructor es la misma declaracion de la clase, por cierto con question de tipo String inicializada en "".

Enum para operaciones

Yo esperaba que camel definiera una ruta por cada operacion que realiza el endpoint, pero para eso necesitas usar el Rest DSL y aqui no lo estamos usando, en java DSL hace una sola ruta para todas la operaciones, por lo que crear una clase que contenga el nombre de las operaciones sera util para evitar errores de tipado y simplificar un poco las cosas, para ello en el paquete question, creamos un nuevo Kotlin File/Class de nombre QuestionOperations:

package com.betotto.question

enum class QuestionOperations(val value: String) {
    GET_ALL_QUESTIONS("getAllQuestions"),
    GET_QUESTION_BY_ID("getQuestionById")
}

Procesando la entrada

Vamos a definir que hacemos con los mensajes que recibamos por el Api definida, para eso usamos Processors y vamos a definir el processor de entrada, que nos permitira preparar los mensajes para ser manipulados como necesitemos, creamos un nuevo Kotlin File/Class QuestionIn en el modulo question:

package com.betotto.question

import org.apache.camel.Exchange
import org.apache.camel.Processor
import org.slf4j.LoggerFactory

class QuestionIn: Processor {

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

    override fun process(exchange :Exchange) {
        val operationName = exchange.`in`.getHeader("operationName") as String
        log.info("operationName: $operationName receiving objects")
        when(operationName) {
            QuestionOperations.GET_QUESTION_BY_ID.value -> {
                val body = exchange.`in`.getBody(List::class.java)
                val question = Question(body[0] as Int)
                exchange.out.body = question
                exchange.out.headers = exchange.`in`.headers
            }
        }
    }
}

Esta clase implementa la interfaz Processor, el cual sobreescribe la funcion process y recibimos el Exchange, en este objeto estan el mensaje de entrada y el mensaje de salida, tomamos la operacion actual del header del mensaje de entrada (funcion del Api correspondiente al tipo de operacion HTTP) y con ella decidimos que hacer dependiendo la operacion, para este proceso en especifico nota que solo estamos creando un objeto Question para la operacion getQuestionById y lo estamos poniendo como body de la salida, tambien copiamos los header de la entrada en los header de la salida.

Asi como se puede ver solo nos enfocamos a la logica en especifico de este proceso, que basicamente es validar los tipos de entrada, dejando las respuestas y decisiones para otras partes del proceso. A mi parecer esto es genial :).

Procesando la salida

Ahora vamos a definir como entregamos los mensajes, para ello creamos otro Processor que construira las respuestas del servicio. Creamos un nuevo Kotlin File/Class QuestionOut:

package com.betotto.question

import com.google.gson.Gson
import org.apache.camel.Exchange
import org.apache.camel.Processor
import org.slf4j.LoggerFactory

import javax.ws.rs.core.Response

class QuestionOut: Processor {

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

    override fun process(exchange: Exchange) {

        val gson = Gson()
        val operationName = exchange.`in`.getHeader("operationName") as String

        log.info("operationName: $operationName sending objects")

        val questions = arrayOf(
            Question(1, "Porque?"),
            Question(2, "Como?"),
            Question(3, "Donde?"),
            Question(4, "Cuando?")
        )

        var status = Response.Status.OK

        var content = ""

        when(operationName) {
            QuestionOperations.GET_ALL_QUESTIONS.value -> {
                content = gson.toJson(questions)
            }
            QuestionOperations.GET_QUESTION_BY_ID.value -> {
                val questionInput = exchange.`in`.body as Question
                val question = questions.filter { q -> questionInput.id == q.id }
                if(question.isNotEmpty()) {
                    content = gson.toJson(question[0])
                } else {
                    content = ""
                    status = Response.Status.NOT_FOUND
                }

            }
        }

        val response = Response.status(status).entity(content).build()
        exchange.out.body = response
    }
}

Aqui es donde se puede ver porque me gusta usar Response, soy de la vieja escuela y me gusta manipular manualmente los status de REST, en este processor creamos la lista de Questions, y dependiendo de la operacion getAllQuestions o getQuestionById, regresamos la lista de questions en formato JSON o filtramos la Question que queremos basados en el Id y regresamos esa question, o un 404 si no existe dicha Question.

Finalmente las ultimas dos lineas crean la respuesta y lo colocan en el mensaje de salida. Me gusto mucho como el filtro de Arrays en Kotlin es muy parecido al de javascript Array.filter(element => isFilter(element)) :P .

Creando la ruta

Ya tenemos todos los elementos necesarios para armar nuestro servicio, simplemente falta crear la ruta, para ello necesitamos un nuevo Kotlin File/Class QuestionRoute

package com.betotto.question

import org.apache.camel.builder.RouteBuilder

class QuestionRoute: RouteBuilder() {

    override fun configure() {
        from("cxfrs://http://{{host}}:{{port}}/wizard/question?resourceClasses=com.betotto.question.QuestionApi")
            .routeId("questionGroupe")
            .routeDescription("Servicios para consultar las preguntas")
            .process(QuestionIn())
            .process(QuestionOut())
    }
}

Decimos que el endpoint es cxfrs sobre http (con jetty como base, Camel sabe solito no necesitamos decirle), usando el host y el puerto del archivo camel.properties, el path es wizard/question y en ese path va a montar nuestro Recurso el cual es com.betotto.question.QuestionApi, le damos un Id a la ruta y una descripcion, finalmente le decimos que el primer proceso es QuestionIn y despues QuestionOut.

De nuestro anterior post sabemos que un HTTP endpoint regresa el ultimo mensaje de la ruta que en nuestro caso es el Response que QuestionOut coloca en el body de salida del exchange. :D

Instalando la ruta

Lo ultimo que hay que hacer es agregar la ruta al contexto de Camel en el archivo Main.kt

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

import com.betotto.question.QuestionRoute

import org.apache.camel.component.properties.PropertiesComponent

fun main(args: Array<String>) {
    val simpleRegistry = SimpleRegistry()
    val context: CamelContext = DefaultCamelContext(simpleRegistry)

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

    context.addRoutes(QuestionRoute())

    context.start()
}

Yo borre la ruta anterior porque solo era de ejemplo. Checa que no tengas problemas de puertos porque creo que no pueden convivir.

Y de a deveras funciona ?

Damos click secundario en el archivo src/main/java/Main.kt -> Run Main.kt la consola debera decir algo asi:

[                          main] log                            INFO  Logging initialized @3098ms to org.eclipse.jetty.util.log.Slf4jLog
[                          main] Server                         INFO  jetty-9.4.6.v20170531
[                          main] AbstractHandler                WARN  No Server set for org.apache.cxf.transport.http_jetty.JettyHTTPServerEngine$1@4068102e
[                          main] AbstractConnector              INFO  Started ServerConnector@31096f76{HTTP/1.1,[http/1.1]}{0.0.0.0:9000}
[                          main] Server                         INFO  Started @3213ms
[                          main] ContextHandler                 INFO  Started o.e.j.s.h.ContextHandler@4ac86d6a{/wizard,null,AVAILABLE}
[                          main] DefaultCamelContext            INFO  Route: questionGroupe started and consuming from: cxfrs://http://0.0.0.0:9000/wizard/question?resourceClasses=com.betotto.question.QuestionApi
[                          main] DefaultCamelContext            INFO  Total 1 routes, of which 1 are started.
[                          main] DefaultCamelContext            INFO  Apache Camel 2.19.4 (CamelContext: camel-1) started in 1.229 seconds

Abrimos nuestro navegador favorito

Get All

Get ById

Not found

Este ultimo checa en la consola de desarrollo el status http

Notas finales

Checa que a veces se hace uso de `in` eso pasa porque in es reservado en Kotlin pero Camel necesita usar eso, asi que Kotlin hace uso de eso para notar la diferencia entre algo que no es Kotlin necesariamente (o sea que es java).

Subi el proyecto a github, este es el commit

Notaras que QuestionIn no es necesario para este post, pero el porque esta ahi sera descrito en el siguiente post.

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