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

Kotlin, Camel y MyBatis

Introducción

Ya con nuestro servicio en forma de mock, podemos empezar a extenderlo un poco mas e incluir mas endpoints. En ese sentido agregaremos consultas a una base de datos, usando MySQL; no sera una tabla muy grande, pero servirá para ejemplificar el proceso. El mapeador de objetos java a SQL sera MyBatis, voy a hacer enojar a muchos tal vez, pero considero que JPA es de los frameworks mas obscuros del mundo Java :D, no se enojen; a mi no me gusta, no significa que no sea util para alguien.

Vamos a obtener los datos de la base para las operaciones que ya creamos, getAll y getByID, asimismo agregaremos una operación mas para insertar Preguntas en la base.

Prerequisitos

Ahora si tenemos mas pre-requisitos:

  • MySQL instalado en donde sea pero que tengamos acceso a una base
  • El conector jdbc-mysql en el repo local de maven
  • El proyecto hasta el POST anterior

La Base de Datos

La base por ahora solo sera una tabla QUESTION, con la estructura siguiente:

CREATE TABLE IF NOT EXISTS `questions`.`QUESTION` (
  `ID_QUESTION` INT NOT NULL AUTO_INCREMENT,
  `TEXT` VARCHAR(100) NOT NULL,
  `CORRECT_ANSWER` INT NULL,
  `CREATION_DATE` DATETIME NOT NULL,
  PRIMARY KEY (`ID_QUESTION`))
ENGINE = InnoDB;

Desarrollo

Dependencias

Vamos a necesitar las siguientes dependencias en nuestro archivo build.gradle:

  • compile "org.apache.camel:camel-mybatis:$camel_version"
  • compile "org.apache.camel:camel-jackson:$camel_version"
  • compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.3'
  • compile group: 'com.fasterxml.jackson.jaxrs', name: 'jackson-jaxrs-json-provider', version: '2.9.3'
  • compile 'mysql:mysql-connector-java:5.1.45'

Y vamos a remover la dependencia de Gson, esto es porque no encontré un json-provider para REST con Gson.

Cambiando el tipo de registro

Para continuar necesitamos actualizar el registro de Camel a que sea de tipo Jndi, de este modo podemos agregar fácilmente beans al contexto. En la carpeta src/main/resources creamos un archivo properties de nombre jndi.properties con el siguiente contenido:

java.naming.factory.initial = org.apache.camel.util.jndi.CamelInitialContextFactory

Mapeo de MyBatis

Creamos una carpeta de nombre mybatis en la carpeta src/main/resources, dentro de esta carpeta creamos un archivo xml question con el siguiente contenido:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.betotto.question.QuestionDao">
    <resultMap type="com.betotto.question.Question" id="mapperQuestion">
        <id property="idQuestion" column="ID_QUESTION" javaType="Int" />
        <result property="text" column="TEXT" javaType="String" />
        <result property="correctAnswer" column="CORRECT_ANSWER" javaType="Int" />
        <result property="creationDate" column="CREATION_DATE" javaType="java.util.Date" jdbcType="TIMESTAMP" />
    </resultMap>

    <select id="getAllQuestions" resultMap="mapperQuestion">
        select ID_QUESTION, TEXT, CORRECT_ANSWER, CREATION_DATE from QUESTION
    </select>

    <select id="getQuestionById" parameterType="Question" resultType="Question" >
        select ID_QUESTION as idQuestion, TEXT as text, CORRECT_ANSWER as correctAnswer, CREATION_DATE as creationDate from QUESTION where ID_QUESTION=#{idQuestion}
    </select>

    <insert id="addQuestion" parameterType="Question" useGeneratedKeys="true" keyProperty="idQuestion" keyColumn="ID_QUESTION">
        insert into QUESTION (TEXT, CORRECT_ANSWER, CREATION_DATE) values (#{text}, #{correctAnswer}, #{creationDate,jdbcType=TIMESTAMP,javaType=java.util.Date})
    </insert>
</mapper>

Nada del otro mundo, es solo el mapeador y las tres operaciones: getAllQuestions - SELECT, getQuestionById - SELECT y addQuestion- INSERT.
Lo que mas me gusta de esto es que los querys están claros, siempre sabemos que query se ejecutara para cada operación, solo leyendo el código, sin esperar a habilitar configuraciones de JPA.

Bueno el Mapper es necesario para cuando queremos consultar varios registros, en tanto que si sabemos que regresara un solo registro, podemos llamar directamente la clase que usara en nuestro caso Question.

Configuración de MyBatis

Ahora vamos a crear la configuración de MyBatis, necesitamos crear un archivo xml llamado SQLMapConfig en la carpeta src/main/resources, agregamos lo siguiente:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <typeAliases>
        <typeAlias alias="Question" type="com.betotto.question.Question"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>

            <dataSource type="POOLED" >
                <property name="driver" value = "com.mysql.jdbc.Driver"/>
                <property name="url" value = "jdbc:mysql://localhost:3306/questions"/>
                <property name="username" value = "questionuser"/>
                <property name="password" value = "12345678"/>
                <property name="poolMaximumIdleConnections" value="10" />
            </dataSource>

        </environment>
    </environments>

    <mappers>
        <mapper resource = "mybatis/question.xml"/>
    </mappers>

</configuration>

Se que MyBatis también permite usar anotaciones en los beans, pero yo preferí dejar la configuración en xml sin una razón en particular, lo que si es que no intente la configuración con anotaciones, por lo que no puedo afirmar que funciona :P, menos con Kotlin.

Aquí definimos el typo Question asociado a nuestra data class, los ambientes que se pueden definir en MyBatis y el mapper Question que definimos en el paso anterior.

Actualizando Question data class

Un tip que me llevo algo de tiempo entender, es que MyBatis espera que las clases Java tengan un constructor vacío y todas las propiedades con getters y setters, pero el modo en que definimos la data class en Kotlin no tiene un constructor vacío, pero gracias a la JVM lo único que tenemos que hacer es asegurarnos de inicializar todas las propiedades de la clase (nos falta el idQuestion) así que solo bastaría inicializar idQuestion con -1.

Sin pensarlo asi, Question quedo immutable en cualquier momento, por tanto si encontramos un objeto Question con Id -1, quiere decir que esa Question no viene de Base de datos.

Actualizando QuestionAPI

En el archivo QuestionAPI, solo hay que agregar la nueva operación:

    @PUT
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    fun addQuestion(question: Question): Response

también modifique el PATH de nuestra API, ahora es "/question" en lugar de "/", esto porque vamos a integrar mas de una ruta en el mismo endpoint http, en la definición de la clase modifica esto:

@Path("/question")
interface QuestionApi {

Actualizando QuestionOperations

Incluimos la nueva operación ADD_QUESTION con valor addQuestion

ADD_QUESTION("addQuestion"),

Actualizando QuestionOut

Ahora tenemos una operación mas, por lo que debemos incluir el apropiado manejo de los status http, simplemente regresamos 201 en lugar de 200 cuando insertamos un objeto en la base de datos,

import com.fasterxml.jackson.databind.ObjectMapper
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 mapper = ObjectMapper();
        val operationName = exchange.`in`.getHeader("operationName") as String

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

        var status = Response.Status.OK
        var content = ""

        when(operationName) {
            QuestionOperations.GET_ALL_QUESTIONS.value -> {
                status = Response.Status.OK
                content = mapper.writeValueAsString(exchange.`in`.body)
            }
            QuestionOperations.ADD_QUESTION.value -> {
                val body = exchange.`in`.getBody(List::class.java)
                status = Response.Status.CREATED
                content = mapper.writeValueAsString(body[0])
            }
            QuestionOperations.GET_QUESTION_BY_ID.value -> {
                if (exchange.`in`.body != null) {
                    content = mapper.writeValueAsString(exchange.`in`.body)
                } else {
                    status = Response.Status.NOT_FOUND
                }
            }
        }

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

También quite el código del filtro y la lista de preguntas, ahora MyBatis y MySQL hacen ese filtro y guardan las preguntas.

Actualizando QuestionRoute

La ruta del modulo Question ahora conectara las consultas de MyBatis.

package com.betotto.question

import org.apache.camel.builder.RouteBuilder

class QuestionRoute: RouteBuilder() {

    override fun configure() {
        from("direct:questionRoute")
            .routeId("questionGroupe")
            .routeDescription("Servicios para consultar las preguntas")
            .process(QuestionIn())
            .choice()
                .`when`(header("operationName").isEqualTo("getAllQuestions"))
                    .to("mybatis:getAllQuestions?statementType=SelectList")
                .endChoice()
                .`when`(header("operationName").isEqualTo("getQuestionById"))
                    .to("mybatis:getQuestionById?statementType=SelectOne")
                .endChoice()
                .`when`(header("operationName").isEqualTo("addQuestion"))
                    .to("mybatis:addQuestion?statementType=Insert")
                .endChoice()
            .end()
            .process(QuestionOut())
    }
}

Para cada operación estamos haciendo el tipo de consulta requerida, addQuestion es de tipo insert, getQuestionById es de tipo SelectOne, y getAllQuestions es de tipo SelectList. Recuerdas que definimos QuestionIn y lo único que hacia era instanciar Question con el id que recibía. Pues bien la razón es que el query de MyBatis para seleccionar una pregunta por Id, recibe un Question, no un entero " " (pudimos dejarlo en entero también pero es para ejemplificar validación de datos).

También modifique el endpoint de entrada de esta ruta, ahora es direct:Question, direct: sirve para conectar rutas en el mismo Contexto de Camel, por tanto esta ruta ya no escucha en http sino que escuchara todos los mensajes que se escriban a direct:Question siempre y cuando sean del mismo Contexto.

Agregando el nuevo endpoint HTTP

El la carpeta src/main/java hay que crear un nuevo Kotlin File/Class llamado CxfrServer:

import org.apache.camel.builder.RouteBuilder

class CxfrServer: RouteBuilder() {

    private val resourceClasses = "com.betotto.question.QuestionApi"
    private val uri= "cxfrs://http://{{host}}:{{port}}/wizard?resourceClasses=$resourceClasses&providers=#restJacksonProviderList"

    override fun configure() {
        from(uri)
            .routeId("Wizard Group rest services")
                .choice()
                    .`when`(header("CamelHttpPath").startsWith("/question"))
                        .to("direct:questionRoute")
                    .endChoice()
                .end()
    }
}

Esta nueva ruta es ahora el endpoint http, he modificado el path, ya no incluye Question, porque eso esta en el API del modulo Question, de momento los resources de este endpoint solo incluye una clase QuestionApi, después voy a agregar otros resources.

Dependiendo del path que viene en el header "CamelHttpPath" enviara, con ayuda de direct:, a la ruta adecuada cada mensaje que llegue, en este caso todos los mensajes que lleguen en /wizard/question serán enviados a direct:questionRoute, recordemos que direct:questionRoute es la ruta del modulo Question.

Modificando Main.kt

Finalmente, podemos actualizar el archivo src/main/java/Main.kt de nuestro pequeño proyecto:

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()))
    val context: CamelContext = DefaultCamelContext(registry)

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

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

    context.start()
}

Hay varios cambios, numero uno estamos registrando el JSON provider para la operación addQuestion, para eso el registry tiene que ser del typo Jndi (el porque del jndi.properties), y el ultimo cambio es que ahora registramos dos rutas, una es el http endpoint del servicio CXF y la otra es la ruta del modulo Question.

Resultados

Bueno ejecutando la aplicación (click secundario src/main/java/Main.kt -> run Main.kt), podemos comprobar que todas nuestras rutas y modificaciones funcionan, en este caso use postman y estos son los resultados

PUT http://0.0.0.0:9000/wizard/question

Body:

{
"text": "Que?",
"correctAnswer": "1"
}

Respuesta: status 201

{
    "idQuestion": 4,
    "text": "Que?",
    "correctAnswer": 1,
    "creationDate": 1516824080557
}

GET http://0.0.0.0:9000/wizard/question

Respuesta: status 200

[
    {
        "idQuestion": 1,
        "text": "Porque?",
        "correctAnswer": 1,
        "creationDate": 1516817309000
    },
    {
        "idQuestion": 2,
        "text": "Cuando?",
        "correctAnswer": 1,
        "creationDate": 1516817356000
    },
    {
        "idQuestion": 3,
        "text": "Como?",
        "correctAnswer": 1,
        "creationDate": 1516817372000
    },
    {
        "idQuestion": 4,
        "text": "Que?",
        "correctAnswer": 1,
        "creationDate": 1516817433000
    }
]

GET http://0.0.0.0:9000/wizard/question/4
Respuesta: status 200

{
    "idQuestion": 4,
    "text": "Que?",
    "correctAnswer": 1,
    "creationDate": 1516817433000
}

GET http://0.0.0.0:9000/wizard/question/7
Respuesta: status 404

Notas finales

El codigo ha sido actualizado este es el commit tiene mucha paja de intellij :P, puedes ignorar todos los archivos de la carpeta .idea .

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