prism

lunes, 28 de noviembre de 2016

Consumir un servicio Web desde COBOL en IBM i 7.1 (AS/400)

Volvemos con una pequeña, sencilla e atractiva nota, que, como verán,  abre posibilidades interesantes los administradores/programadores de IBM i

Comunicar una aplicación que se ejecuta en un sistema AS/400 con el mundo exterior -entiéndase exterior como aplicaciones externas al AS/400- no esta tarea fácil, especialmente si tomamos en cuenta que el programa que ocupa "hablar" con otros podría estar construido en lenguajes como COBOL o RPG; lenguajes que, probablemente no fueron concebidos para este tipo de tareas y aunque se han ido adaptando, no lo han echo tan bien como uno quisiera. Cabe decir que la interacción de esta plataforma y otros sistemas no es algo nuevo, se viene haciendo desde hace mucho tiempo con tecnologías como IBM Web sphere MQ, la cual funciona bastante bien y sigue siendo una de las principales formas de comunicación, sin embargo, desde mi punto de vista, tiene algunos inconveniente, entre ellos:

  • Su modelo es escencialmente shut-and-forget, es decir, es asincrónico, lo cual no es lo óptimo en ciertos escenarios.
  • Licenciado. No es barato.
  • Se debe  instalar al menos el cliente de MQ en los sistemas con los que se quiera comunicar.
  • El API para leer y escribir los mensajes no es la más sencilla de utilizar. 

Con un poco de contexto vamos a entrar en materia. Probablemente hayan escuchado hablar de servicios Web (Web services), esta tecnología ha implantado un mecanismo de comunicación entre aplicaciones que es sencillo, barato, confiable, de buen rendimiento y lo más importante; ampliamente aceptado por la industria, es decir, prácticamente todos los lenguajes de programación modernos pueden comunicarse mediante servicios Web, y el AS/400 NO es la excepción. Desde hace algunos años IBM suministra con sus sistemas i, una serie de librerías que facilitan el consumo de servicios Web desde programas construidos en lenguajes como COBOL, RPG y seguro que algunos otros. Estas librerías se denominan Web Services Client for ILE.

Básicamente, estas librerías permiten tomar una definición de un servicio Web (WSDL) y mediante sencillos comandos, generan los programas (fuentes y objetos) que incluyen todo lo necesario para consumir el servicio Web. Las fuentes pueden generarse en lenguaje C, C+++ o RPG (Lamentablemente para algunos, COBOL no está entre las opciones). Estos programas son una clase de intermediario, ya que serán invocados desde nuestra aplicación y serán ellos los que se comuniquen con el servicio Web. En el siguiente diagrama estos programas generados en base al WSDL están representado por el cuadro verde.

Interacción de componentes utilizando el Web Service Client for ILE

En este mismo diagrama nos encontramos un componente denominado "Wrapper en C". Este es un C/C++ desarrollado por el programador (lamentablemente no se genera mediante el API) y cuya misión es encapsular la invocación al programa intermedio generado mediante Web Services Client for ILE. En este wrapper se oculta la complejidad (tipos de datos, referencias, conversiones, manejo de excepciones, etc) propia de la ejecución del programa intermedio, exponiendo así una interfaz sencilla al COBOL. Si nuestra aplicación se desarrolla en lenguaje C/C++/RPG, este Wrapper es opcional, sin embargo, para COBOL es obligatorio.

Dicho lo anterior, continuamos con un ejemplo. Para esta nota tomaremos un servicio Web público y llevaremos a cabo todos los pasos necesarios para consumirlo desde un programa COBOL. Iniciamos: 

Generar programa intermedio


El servicio Web seleccionado es muy sencillo y está accesible publicamente, su URL es: http://www.w3schools.com/xml/tempconvert.asmx. Este contiene dos métodos; CelsiusToFahrenheit, que permite convertir de grados celcius a Fahrenheit y otro denominado FahrenheitToCelsius que hace la conversión inversa. Desarrollaremos el ejemplo basados en el método CelsiusToFahrenheit.

Para acceder el WSDL del servicio hay varias opciones, una es mediante el URL, en la dirección http://www.w3schools.com/xml/tempconvert.asmx?wsdl Esta es la mejor opción, sin embargo, si por alguna razón el URL no se encontrara disponible, dejo una copia en google drive.

Con el WSDL en mano, el segundo paso es generar mediante la librería Web Services Client for ILE el programa intermedio (cuadro verde del diagrama) que le "hablará" al servicio Web. Esto se realiza con el comando wsdl2ws.sh

La sintaxis del comando es la siguiente:

wsdl2ws.sh [argumentos] WSDL


Donde:

WSDL: Corresponde al URI del archivo WSDL, puede ser directamente el URL (http://...) o una ruta en el sistema de archivos en caso de tener el WSDL local.

argumentos: 

-l
Indica el lenguaje en el cual será generado el programa intermedio. El valor por defecto es C++. Los posibles valores son c, c++ y rpg

-o 
Indica el directorio donde serán almacenados los fuentes generados. Recordemos que este comando genera además de los programas compilados las fuentes respectivas. Esta es un ruta del IFS, no una biblioteca

-s
Especifica el nombre que tendrá el programa generado, además de la biblioteca.. Este parámetro debe tener la forma /QSYS.LIB/MILIBRERIA.LIB/MIPROGRAMA.SRVPGM, donde MILIBRERIA.LIB es la librería donde se almacenará el programa y MIPROGRAMA.SRVPGM es el nombre que tendrá el programa generado.


-d
Genera el objeto con las vistas de depuración activada. Esto para posibles depuraciones.

El WSDL lo coloqué en local en la ruta del IFS de mi AS/400 (Bueno, no es mio ^_^) /home/INNOVAI bajo el nombre tempconvert.wsdl, en esta misma carpeta quedarán los fuentes generados. La biblioteca donde se almacenarán los programas (objetos) generados se denomina SQLUTIL y el programa generado se llamará TMPCNVGEN.SRVPGM.  Por lo tanto, los valores de los argumentos del comando wsdl2ws.sh serán los siguientes:

WSDL: /home/INNOVAI/tempconvert.wsdl

-l c++
-o /home/INNOVAI/
-s /QSYS.LIB/SQLUTIL.LIB/TMPCNVGEN.SRVPGM


Ya tenemos casi todo listo para ejecutar el comando, solo falta conocer dos simples detalles: donde se encuentra el archivo  wsdl2ws.sh y desde donde ejecutarlo. En sistemas IBM i 7.2 el archivo wsdl2ws.sh se encuentra en la dirección del IFS /QIBM/ProdData/OS/WebServices/V1/client/bin y para ejecutarlo se debe iniciar el shell mediante el comando STRQSH. Una vez en el shell podemos ir a la carpeta indicada mediante el comando cd /QIBM/ProdData/OS/WebServices/V1/client/bin (Nótese que cd debe estar escrito en minúscula). Una vez ejecutado el cd, ejecutamos el comando ls para corroborar que estamos posicionados en la carpeta correcta, ya que deberían aparecer solo dos archivos: wsdl2rpg.sh, wsdl2ws.sh. Resumiento, la secuencia entonces será la siguiente:

  • Desde la consola del sistema IBM i, ejecutamos el comando STRQSH para iniciar el SHELL.
  • En el SHELL, ejecutamos cd /QIBM/ProdData/OS/WebServices/V1/client/bin para colocarnos en la carpeta donde se encuentra el archivo wsdl2ws.sh
  • Corroboramos con el mandato ls que aparezca el archivo wsdl2ws.sh. La salida del mandato ls debe ser similar a la siguiente:
Salida esperada del comando ls

Ahora si, ya tenemos todo listo para ejecutar el comando wsdl2ws.sh
Este va ser el siguiente:

wsdl2ws.sh -lc++ 
-o"/home/INNOVAI/"                               
-s/QSYS.LIB/SQLUTIL.LIB/TMPCNVGEN.SRVPGM 
"/home/INNOVAI/tempconvert.wsdl"

Lo copiamos al SHELL y presionamos "Enter".

Comando antes de ser ejecutado
Antes de ver el resultado de la ejecución, nótese un par de cosas sobre la sintaxis del comando; primero, el valor del argumento debe ir pegado al argumento tal cual, por ejemplo, entre -l y c++ no deben haber espacios en blanco, caso contrario se generaría un error de sintaxis. Segundo, algunos argumentos van entre " ", esto para evitar problemas con rutas que contengan espacios en blanco. Dicho lo anterior, volvemos a la ejecución del comando, esta debería imprimir en pantalla lo siguiente:

Salida de la ejecución del comando wsdl2ws.sh
Como se puede ver, el mensaje indica que tanto la generación de código como la creación del programa se efectuaron satisfactoriamente. Vamos a ver los objetos generados, primero la fuentes. En la carpeta /home/INNOVAI se generaron los siguiente archivos:

ws.cl
TempConvertSoap.hpp
TempConvertSoap.cpp

El archivo ws.cl es un CL con los comandos de compilación del fuente contenido en TempConvertSoap.cpp. No requiere especial atención.

El archivo TempCovertSoap.hpp contiene la definición de la clase. Dado que esta definición se generó en base a un WSDL, tendremos un prototipo (también llamados definiciones de función) por cada método que este contenga. Revisemos un poco más a fondo este archivo. Se adjunta a continuación el código:


/*
 * This file was auto-generated by the Axis C++ Web Service Generator (WSDL2Ws)
 * This file contains Client Stub Class for remote web service 
 */

#if !defined(__TEMPCONVERTSOAP_CLIENTSTUB_H__INCLUDED_)
#define __TEMPCONVERTSOAP_CLIENTSTUB_H__INCLUDED_

#include <axis/client/Stub.hpp>
#include <axis/OtherFaultException.hpp>
#include <axis/ISoapAttachment.hpp>
#include <axis/ISoapFault.hpp>

AXIS_CPP_NAMESPACE_USE


class TempConvertSoap :public Stub
{
public:
 STORAGE_CLASS_INFO TempConvertSoap(const char* pchEndpointUri, AXIS_PROTOCOL_TYPE eProtocol=APTHTTP1_1);
 STORAGE_CLASS_INFO TempConvertSoap();
public:
 STORAGE_CLASS_INFO virtual ~TempConvertSoap();
public: 
 STORAGE_CLASS_INFO xsd__string FahrenheitToCelsius(xsd__string Value0);
 STORAGE_CLASS_INFO xsd__string CelsiusToFahrenheit(xsd__string Value0);
};

#endif /* !defined(__TEMPCONVERTSOAP_CLIENTSTUB_H__INCLUDED_)*/


Destacar que la clase incluye dos constructores, uno sin parámetros y otro que recibe el URL al cual se debe enviar la solicitud, además se observan los dos métodos, entre estos, el que concentra nuestra atención: CelsiusToFahrenheit. Este recibe una hilera de caracteres con el valor de los grados a convertir y retorna el resultado en otra hilera. Sobre el tipo utilizado; xsd__string, solo mencionar que es equivalente a char*.

El archivo  TempConvertSoap.cpp contiene la implementación del llamado al servicio Web. Si NO tenemos interés en entender como se invoca un servicio Web desde C++, no será necesario revisar este archivo.

Como se indicó previamente, el comando wsdl2ws.sh genera además de las fuentes, los objetos resultantes de la compilación de estas fuentes, por tanto es importante corroborar que los objetos se encuentren en la biblioteca indicada. La siguiente imagen muestra que efectivamente el objeto se generó:

Objetos generados mediante wsdl2ws.sh

Nota: Quizás se preguntarán porqué al ejecutar el comando wsdl2ws.sh elegimos como lenguaje de generación C++ (-lc++), pues bueno, es más una cuestión de gustos que cualquier otra cosa, ya que con C o RPG vamos a lograr lo mismo. Por otro lado, concuerdo con un artículo que leí en ibmdeveloperworks donde el autor explicaba que el modelo de un WSDL se adapta mejor a un lenguaje orientado a objetos que a uno estructurado y por lo tanto es mejor y más sencillo utilizar C++.

Wrapper en C


El objeto generado en el paso anterior (TMPCNVGEN.SRVPGMcorresponde en el diagrama al componente denominado Programa intermedio generado mediante Web Services Cliente for ILE, es decir, es el programa encargado de "conversar" con el servicio Web. Ahora bien, como se mencionó líneas atrás, nuestro programa COBOL no le hablará a este componente directamente, sino que lo hará mediante el componente denominado wrapper en C. El siguiente paso será entonces construir este wrapper.

El fuente del wrapper lo ubicaremos en el IFS, en la carpeta /home/INNOVAI, bajo el nombre TempConvertWrapper.c.

Se adjunta el código respectivo:


#include <string.h>                                      
#include <stdlib.h>
#include "TempConvertSoap.hpp"                            
using namespace std;                                      
                                                          
#pragma map(CelsiusToFahrenheit(char[4]),"CNVRTCTOF")
                                                          
double CelsiusToFahrenheit(char val[4])
{
  try
  {
    TempConvertSoap *proxy = new TempConvertSoap("http://www.w3schools.com/xml/tempconvert.asmx" ,APTHTTP1_1);
    return strtod(proxy->CelsiusToFahrenheit(val),NULL);
  }
  catch(SoapFaultException& sfe)
  {
  cout << "SoapFaultException: " << sfe.getFaultCode() << " " << sfe.what() << endl;
  }
  catch(exception& e) {
   cout << "Unknown Exception: " << e.what() <<  endl;
  }
}                                                        



Esto ya lo dije, pero vamos de nuevo: el wrapper debe ser un programa pequeño, claro, conciso y enfocado principalmente en encapsular en una definición lo más sencilla posible la invocación a los métodos del programa generado mediante el wsdl2ws.sh, con el fin de "facilitarle" las cosas a COBOL.

El wrapper anterior incluye solamente la función de nuestro interés (CelsiusToFahrenheit) que recibe una hilera de caracteres con los grados en celsius y retorna el resultado en grados fahrenheit en un tipo double. Como se puede observar, esta función "oculta" la complejidad de definir el puntero, instanciar el objeto (new), ejecutar el llamado a través del puntero, manejar excepciones, convertir tipos de datos, etc.  Obteniendo de esta forma un programa muy fácil de invocar.

Algo muy importante en el wrapper y que se debe incluir en cualquier implementación de este tipo, es el uso del try/catch, dada la alta probabilidad de que existan errores durante el consumo de un servicio servicios Web. Si no controlamos la excepción con este mecanismo, el resultado de un error va ser la cancelación del programa mediante una señal SIGABRT y un error largo e incomprensible en el spool. Claro está, en lugar de solamente imprimir el mensaje de error, lo ideal es retornar alguna bandera al programa invocador para que sepa que algo sucedió, sin embargo, no vamos a incluirlo para no complicar el ejemplo.

También es importante advertir la clausula #pragma, la cual le indica al compilador que las referencias a la función de nombre CelsiusToFahrenheit deben ser convertidas al nombre CNVRTCTOF y con este último la exporta, por tanto, la función será referenciada desde COBOL con el nombre CNVRTCTOF. Honestamente no estoy seguro si se puede exportar una función de otra forma, lo que si es cierto es que sin esta cláusula #pragma, la compilación del programa final no se puede realizar

Procederemos entonces a compilar el wrapper. Este debe ser compilado en un módulo, mediante el comando CRTCPPMOD. El detalle más importante en este proceso de compilación es agregar los directorios de include correctos, para que así el compilador sepa donde localizar los distintos encabezados. En primer lugar, se debe incluir el directorio donde se encuentra el encabezado TempCovertSoap.hpp, sin embargo, como este se encuentra en la misma ruta donde colocamos los fuentes del wrapper (/home/INNOVAI), no hará falta incluirlo. La ruta que si es indispensable incluir, es la que contiene los encabezados propios de la librería Web Services Client for ILE, donde se encuentra por ejemplo la definición del tipo xsd__string. En equipos con IBM i 7.2 esta ruta es: /QIBM/ProdData/OS/WebServices/V1/client/include

Con la información anterior, podemos definir nuestro comando de compilación del wrapper. Este va ser el siguiente:


CRTCPPMOD MODULE(SQLUTIL/TMPCNVWRPR) 
          SRCSTMF('/home/INNOVAI/TempConvertWrapper.c') 
          INCDIR('/QIBM/ProdData/OS/WebServices/V1/client/include')

Analicemos rápidamente el comando. En primer lugar tenemos el argumento MODULE, este indica el nombre del módulo resultante de la compilación, así como su biblioteca destino. El argumento SRCSTMF especifica la ruta del archivo fuente a compilar. Por último, el argumento INCDIR corresponde al directorio include explicado en el párrafo anterior. Si todo sale bien, se debería presentar un mensaje como el siguiente:

El módulo TMPCNVWRPR se ha creado en la biblioteca SQLUTIL....

Y debe por tanto, existir un objeto de nombre CNVTMPWRPR de tipo *MODULE y con atributo CPPLE en la biblioteca SQLUTIL.

Cliente en COBOL


Ya casi estamos listos, lo único que falta es el programa COBOL que invocará al Wrapper. A continuación el código:


       IDENTIFICATION DIVISION.                                   
       PROGRAM-ID. TMPCNVCLI.                                     
       AUTHOR. OLMAN CARBALLO.                                    
      *                                                                  
       DATA DIVISION.                                             
        WORKING-STORAGE SECTION.                                  
         01 GRADOSF COMP-2 USAGE IS DISPLAY.                      
         01 GRADOSC PIC X(4).                                     
         01 GRADOSFDISP PIC Z(9).9(9).                               
      *                                                           
       PROCEDURE DIVISION.                                        
           INITIALIZE GRADOSC                                     
           INITIALIZE GRADOSF                                     
           MOVE "0020" TO GRADOSC.                                
           CALL PROCEDURE "CNVRTCTOF" USING GRADOSC               
                                     RETURNING INTO GRADOSF.      
           MOVE GRADOSF TO GRADOSFDISP.                              
           DISPLAY GRADOSC " GRADOS C CONVERTIDOS A F = " GRADOSFDISP. 


Como se puede observar, este es un programa muy sencillo que consta básicamente de la definición de variables, invocación al wrapper e impresión del resultado. Hay unos pocos detalles a destacar,  entre ellos:

  • El CALL  incluye la cláusula PROCEDURE, para indicar que se va invocar una función.
  • El nombre de la función invocada en el CALL concuerda con el valor indicado en la cláusula #pragma del wrapper.
  • Antes de imprimir el resultado, este se pasa a la variable GRADOSFDISP. La única razón de de esta asignación es darle un formato al valor antes de imprimirlo. Es algo así como una máscara.
Este programa debe ser compilado como un módulo, por lo tanto se debe utilizar la opción 15 o directamente el comando CRTCBLMOD. El módulo resultante se ubicará en la biblioteca SQLUTIL. El comando de compilación es el siguiente:


CRTCBLMOD MODULE(SQLUTIL/TMPCNVCLI)
          SRCFILE(SQLUTIL/QCBLSRC)
          SRCMBR(TMPCNVCLI)       
          REPLACE(*YES)       

Como se puede observar, el miembro se ubica en un archivo denominado QCBLSRC en la biblioteca SQLUTIL y será compilado con el nombre TMPCNVCLI en la misma biblioteca.

Si la compilación es correcta, se debería mostrar el siguiente mensaje:

Se ha creado el módulo TMPCNVCLI en la biblioteca SQLUTIL...

Realizado lo anterior, deberíamos tener los siguientes objetos en la biblioteca SQLUTIL:

Módulos generados en la biblioteca SQLUTIL
Estos son:

  • TMPCNVGEN: Programa de servicio generado mediante WSDL2WS.SH
  • TMPCNVWRPR: Wrapper en C
  • TMPCNVCLI: Programa cliente en COBOL
Hasta el momento, todos los objetos son módulos o programas de servicio, por tanto, no se pueden invocar directamente mediante un CALL. Para ejecutarlos, es necesario "juntarlos" en un objeto de tipo *PGM, lo que lograremos mediante el comando CRTPGM, el cual recibe una lista de módulos, una lista de programas de servicio y genera un *PGM. A continuación el comando:


CRTPGM PGM(SQLUTIL/TMPCNV) 
       MODULE(SQLUTIL/TMPCNVCLI SQLUTIL/TMPCNVWRPR)
       BNDSRVPGM((SQLUTIL/TMPCNVGEN))  

Este generará nuestro *PGM en la biblioteca SQLUTIL bajo el nombre TMPCNV y utilizará los módulos y programas de servicio generados previamente. Ejecutamos el comando y verificamos que la salida sea la siguiente:

Programa TMPCNV creado en biblioteca SQLUTIL.

Este mensaje quiere decir que el *PGM se creó correctamente y por tanto estamos listos para ejecutar nuestro COBOL. Esto mediante el siguiente comando


   CALL PGM(SQLUTIL/TMPCNV)


Y deberíamos obtener la siguiente impresión en pantalla:

Salida del programa SQLUTIL/TMPCNV

Si nuestra salida es similar a la siguiente, felicidades! logramos consumir un servicio Web desde COBOL. Espero que les haya gustado la nota!

viernes, 5 de agosto de 2016

Construir un programa en COBOL ILE y asociarlo a una función SQL en DB2 para sistemas IBM i 6.1

Como bien lo dice el título de la nota, el objetivo aquí es explicar los pasos necesarios para construir un programa de COBOL y asociarlo a una función de DB2. Esto permite que el programa pueda ser utilizado como cualquier otra función propia del motor de base de datos y por lo tanto se pueda invocar por ejemplo desde un cliente jdbc, una vista, un procedimiento almacenado u otra función.

Hace un tiempo publiqué una entrada muy similar a esta, pero en lugar de utilizar COBOL, se utilizó el lenguaje C. A fin de cuentas el procedimiento es muy similar (casi es una copia), sin embargo hay un par de diferencias muy importantes y poco documentadas que justifican esta nota.

Para no complicar mucho el asunto, el ejemplo va ser el mismo de la nota en lenguaje C, es decir, se va crear una función llamada EsNumerico, que determinará si una hilera de caracteres contiene solamente números. De ser así, retornará 1, de lo contrario 0. Por ejemplo, al invocar EsNumerico('ABC012') el resultado será 0, mientras que EsNumerico('123456') retornará 1.

Entrando en materia, lo primero será definir nuestra función SQL, buscando determinar los parámetros que va recibir y el tipo de dato de retorno. Hasta que este paso no esté completado, no es buena idea iniciar el programa en C.

El SQL de la función va ser el siguiente:

 CREATE FUNCTION SQLUTIL/EsNumerico(VARCHAR(50))
  RETURNS INTEGER
  LANGUAGE COBOLLE
  EXTERNAL NAME 'SQLUTIL/CBLESNUM01'
  DETERMINISTIC
  NO SQL
  PARAMETER STYLE SQL
  NO EXTERNAL ACTION
La función se puede definir mediante la consola de comandos SQL (STRSQL), o tambien se puede usar la herramienta Open Source SQuirreL o el System i navigator. En la consola TN5250, el separador entre la biblioteca y el nombre de la función será / , mientras que en SQuirreL y en el System i navigator será un punto. La cláusula EXTERNAL NAME tendrá el mismo separador sin importar desde que herramienta sea ejecutado.

Algunos puntos importantes sobre esta declaración:
  • Los parámetros no presentan nombre, solo tipo.
  • LANGUAGE COBOLLE especifica que el lenguaje es COBOL ILE. 
  • La cláusula EXTERNAL NAME indica cual es el programa externo (objeto) que se va a ejecutar. El valor se conforma de la siguiente forma: BIBLIOTECA/PROGRAMA. 
  • La cláusula LANGUAGE especifica en que lenguaje está desarrollado en programa externo. Algunos posibles valores son: C, C++, CL, COBOL, RPG, JAVA, entre otros. 
  • DETERMINISTIC le indica al motor de base de datos, que esta función siempre retornará el mismo resultado para los mismos parámetros de entrada. Es decir, si se invoca EsNumerico('123') 1000 veces, el resultado siempre va ser 1. Esta información la usa db2 para optimización.
  • PARAMETER STYLE corresponde a la cantidad y tipo de los parámetros que el motor de base de datos transferirá al programa externo. Para programas desarrollado en COBOL se puede utilizar el valor SQL (como este caso) o GENERAL, sin embargo, este último no lo he probado. Al utilizar el valor SQL, se debe preparar el programa externo para que reciba una serie de parámetros adicionales a los definidos en la declaración de la función. Esto se explicará más adelante.
Entendido lo anterior, ejecutamos el SQL para crear la función. En la herramienta STRSQL el resultado se muestra en la siguiente imagen:

Creación de la función mediante el STRSQL

Ahora que ya está creada la función y se conoce por lo tanto la cantidad de parámetros, su tipo de dato y el tipo de dato de retorno, es momento de construir la función en COBOL.

Para definir una función en este lenguaje en un sistema IBM i, se crea un miembro de tipo CBLLE mediante la herramienta PDM (WRKMBRPDM) y la opción F6 (Crear). Esto muestra la siguiente pantalla:

Crear miembro de tipo CBLLE mediante STRSEU
Al aceptar el comando, se muestra el editor de código fuente. El código de nuestro programa será el siguiente:


 IDENTIFICATION DIVISION.                           
 PROGRAM-ID. CBLESNUM01.                            
 AUTHOR. OLMAN.                                     
*                                                   
 DATA DIVISION.                                     
  LINKAGE SECTION.                                  
   01 HILERAVALIDAR.                                
     49 TAMANIO              PIC S9(4) USAGE BINARY.
     49 CONTENIDO            PIC X(50).             
   01 RESULTADO              PIC S9(9) COMP-4.      
   01 IN-IND                 PIC S9(4) COMP-4.      
   01 OUT-IND                PIC S9(4) COMP-4.      
   01 SQLSTATE               PIC X(5).              
   01 FNAME                  PIC X(517).            
   01 ESPENAME               PIC X(128).            
   01 MSGTEXT                PIC X(1000).           
*                                                           
 PROCEDURE DIVISION USING HILERAVALIDAR,                    
                          RESULTADO,                        
                          IN-IND,                           
                          OUT-IND,                          
                          SQLSTATE,                         
                          FNAME,                            
                          ESPENAME,                         
                          MSGTEXT.                          
                                                                                             
     IF TAMANIO > 0 AND CONTENIDO(1:TAMANIO) IS NUMERIC THEN
        MOVE 1 TO RESULTADO                                 
     ELSE                                                   
        MOVE 0 TO RESULTADO                                 
     END-IF.                                                



Este código puede tomarse tal cual y pegarse en la consola TN5250 y debería compilar sin errores.
En el ejemplo se observa que el programa recibe la hilera, valida que tenga al menos un carácter y que estos sean numéricos. Esto último lo efectúa mediante la función nativa "IS NUMERIC".

Como se explicó previamente, al utilizar el valor SQL en la cláusula PARAMETER STYLE del CREATE FUNCTION, se debe incluir a el linkage una serie de parámetros adicionales requeridos por el motor de base de datos para poder interactuar con el programa.  Estos contienen información de control y de invocación. A continuación se detalla la lógica que rige la definición de estos parámetros:
  • Los primeros N parámetros del linkage section, corresponden a los N parámetros de la función SQL. En este caso, como la función definida en el CREATE FUNCTION solo tiene un parámetro de tipo VARCHAR, entonces este será recibido en el primer parámetro del programa COBOL (HILERAVALIDAR). Este parámetro requiere especial atención, ya que se conforma de dos partes: TAMANIO y CONTENIDO. ¿Porqué un solo parámetro en la función SQL se convierte en dos parámetros de COBOL? Pues la respuesta se encuentra en el tipo de datos utilizado en el CREATE FUNCTION. Si el tipo de dato es VARCHAR o VARGRAPHICS entonces el parámetro en COBOL deberá definirse con estos dos elementos. El primero (TAMANIO) indica el tamaño real de la hilera de caracteres, esto ya que un parámetro de tipo VARCHAR aunque se define con un tamaño máximo, el tamaño en tiempo de ejecución puede variar desde cero hasta el tamaño máximo definido, por eso la variable TAMANIO tendrá el largo real del parámetro definido. Por ejemplo, si se invoca la función de la siguiente forma: EsNumerico('ABC123'),  entonces el programa COBOL recibirá el valor 6 en la variable TAMANIO mientras que el valor ABC123 será recibido en el parámetro CONTENIDO. Este último como se puede observar en los ejemplos, tiene el mismo tamaño que el parámetro de la función SQL; PIC X(50) / VARCHAR(50). Para más detalle sobre la compatibilidad de tipos de datos entre SQL y COBOL, revisar el siguiente enlace.
  • Seguido a los parámetros de entrada, está el parámetro que contendrá el valor de salida (RESULTADO). Dado que la función SQL se definió como de tipo entera (RETURNS INTEGER), entonces el parámetro en COBOL será de tipo PIC S9(9) COMP-4. Esto según las tabla de compatibilidad de IBM.
  • Después del parámetro de salida, se deben definir N parámetros de tipo PIC S9(9) COMP-4, donde N está representada por la cantidad de parámetros de la función SQL. Estos son indicadores que señalan si en la invocación de la función alguno sus parámetros se envió con valor null. Si el valor de estos indicadores es menor que 0, quiere decir que el parámetro al cual este indicador está asociado viene con valor null. Si el valor del indicador es 0 o mayor, el parámetro tienen un valor distinto de null. Por ejemplo, si la función se invoca de la forma EsNumerico(ValorA) y ValorA es null, entonces el programa COBOL recibirá el parámetro IN-IND con un valor menor a 0. Especial énfasis en la relación entre la cantidad de parámetros y la cantidad de indicadores:  por cada parámetros en el CREATE FUNCTION, se debe incluir en el COBOL un indicador, cuyo tipo debe ser de tipo PIC S9(9) COMP-4.
  • Seguido a estos indicadores de entrada, se deben definir un solo parámetro de tipo PIC S9(9) COMP-4 que corresponde a un indicador de salida. Este indicador sirve para indicarle al DB2 si el resultado de la función es null o no, es decir, este indicador está pensado para ser manipulado en el COBOL; si se le asigna un valor inferior a 0, el DB2 interpretará que la función tuvo como resultado null, si es 0 o mayor, entonces DB2 interpretará que el resultado es distinto de null. Si el indicador no es modificado en el programa (como en el ejemplo que se muestra lineas atrás) entonces su valor siempre será 0 o mayor, por lo tanto el resultado de la función siempre será distinto de null
  • Por último, siempre se deben definir las variables SQLSTATE, FNAME,  ESPENAME y MSGTEXT (Los nombres no importan, lo importante es su tipo de dato). SQLSTATE sirve para retornar códigos de excepción, FNAME contiene el nombre de la función SQL que está haciendo el llamado, ESPENAME contiene un nombre específico y MSGTEXT permite retornar mensajes de excepción. Realmente para casos  tan simples como el de este artículo, son campos poco relevantes, sin embargo siempre deben incluirse en el programa.
Teniendo esto claro, podemos continuar con la compilación de la función.

En los sistemas IBM i, la compilación se puede efectuar con opción 14 para crear un objeto de tipo *PGM u opción 15 para crear uno de tipo *MODULE. La opción a seleccionar va ser la 14, dado que el programa final utilizado por el SQL debe ser de tipo *PGM. Entonces, compilamos el miembro mediante la opción 14, tal y como se muestra en la siguiente imagen:

Creación de un objeto a partir de un miembro en lenguaje cobol ILE

Antes de oprimir "Enter" es necesario establecer una opción de vital importancia para el funcionamiento de la función. Esta opción se denomina Grupo de activación  (ACTGRP) y se edita oprimiendo F4 (siempre con el 14 digitado a la par del miembro). Esto muestra una pantalla adicional, en la cual se debe presionar F10 (mostrar parámetros adicionales) y bajar (Av Pág) hasta encontrar la opción que se muestra en la siguiente imagen:

Editar el grupo de activación antes de compilar
Esta opción por defecto tiene el valor QILE, el cual debe ser cambiado a *CALLER. Este paso es fundamental, ya que si no se efectúa, cualquier invocación a la función, sin importar el cliente que se utiliza, termina con un error tal que cierra la conexión.

Establecida esta opción, presionamos "Enter" para iniciar la compilación. Si todo sale bien, se debe haber creado un objeto de nombre CBLESNUM01 y de tipo *PGM en la biblioteca SQLUTIL, tal y como se muestra en la siguiente imagen:

Objeto creado después de la compilación

Si todos los pasos se han seguido, en este punto la función SQL ya debería funcionar. Para verificarlo, se puede iniciar la consola de comandos SQL (STRSQL) y ejecutar la siguiente sentencia:

SELECT ESNUMERICO('123') FROM SYSIBM/SYSDUMMY1

Antes de esto, hay que cerciorarse que la biblioteca donde se haya definido la función SQL (en el ejemplo SQLUTIL) se encuentre en nuestra lista de bibliotecas. Si no lo está, agregarla mediante el comando ADDLIBLE NombreBiblioteca

El resultado debería ser igual al mostrado en la siguiente imagen:

Resultado de la función en la consola SQL 

Hasta aquí esta nota. Espero se entienda!



jueves, 26 de mayo de 2016

Construir un programa en C y asociarlo a una función SQL en DB2 para sistemas IBM i

Desarrollé este pequeño apunte para recordar como se construye, depura y asocia una función en lenguaje C al motor de base de datos DB2 en sistemas IBM i 6.1. Esto, aunque ligeramente complejo, permite solucionar problemas que con las funciones nativas del motor, quizás no sería posible.

Una vez publicadas, estas funciones pueden utilizarse de forma transparente, sin ninguna consideración adicional desde otros procedimiento almacenado o funciones, ya que para el desarrollador que las utiliza, no presentan diferencia algunas con las demás.

El ejemplo a utilizar corresponde a una función que se denominó EsNumerico, la cual recibe una hilera de caracteres y verifica si todos los caracteres son números. De ser así, retorna 1, de lo contrario 0.

Por lo tanto, al invocar EsNumerico('ABC012') el resultado será 0, mientras que EsNumerico('123456') retornará 1.

Entrando en materia, lo primero será definir nuestra función SQL, buscando determinar los parámetros que va recibir y el tipo de dato de retorno. Hasta que este paso no esté completado, no es buena idea iniciar el programa en C.

El SQL de la función va ser el siguiente:



 CREATE FUNCTION SQLUTIL.EsNumerico(VARCHAR(50)) 
 RETURNS INTEGER
 EXTERNAL NAME 'SQLUTIL/ESNUMERICO(EsNumerico)'
 LANGUAGE C    
 DETERMINISTIC
 NO SQL
 PARAMETER STYLE SQL
 NO EXTERNAL ACTION




La función se puede definir mediante la consola de comandos SQL (STRSQL), o como en mi caso, mediante la herramienta Open Source SQuirreL. En la consola TN5250, el separador entre la biblioteca y el nombre de la función será / , mientras que en SQuirreL será como en la imagen. La cláusula EXTERNAL NAME tendrá el mismo separador sin importar donde se ejecute.

Algunos puntos importantes sobre esta declaración:

  • Los parámetros no presentan nombre, solo tipo.
  • La cláusula EXTERNAL NAME indica cual es el programa externo que se va a ejecutar. Se conforma de lo siguiente: 'ESQUEMA/PROGRAMA(NOMBRE_FUNCION). En el ejemplo en cuestión, la función se denomina EsNumerico, pertenece a un programa desarrollado en lenguaje C (podría ser cualquier otro lenguaje soportado por la plataforma) y compilado bajo el nombre ESNUMERICO en la librería SQLUTIL
  • La cláusula LANGUAGE especifica en que lenguaje fue desarrollado en programa externo. Algunos posibles valores son: C, C++, CL, COBOL, RPG, JAVA, entre otros. 
  • DETERMINISTIC le indica al motor de base de datos, que esta función siempre retornará el mismo resultado para los mismos parámetros de entrada. Es decir, si se invoca EsNumerico('123') 1000 veces, el resultado siempre va ser 1. Esta información la usa db2 para optimización.
  • PARAMETER STYLE corresponde a la cantidad y tipo de los parámetros que el motor de base de datos transferirá al programa externo. Para programas desarrollado en C se puede utilizar el valor SQL (como este caso) o GENERAL, sin embargo, este último no lo he probado. Al utilizar el valor SQL, se debe preparar el programa externo para que reciba una serie de parámetros adicionales a los definidos en la declaración de la función. Esto se explicará más adelante.

Ahora que ya están definidos los parámetros y el tipo de dato de retorno, es momento de construir la función en C.

Para definir una función en este lenguaje en un sistema IBM i, se crea un miembro de tipo C mediante la herramienta PDM (WRKMBRPDM) y la opción F6 (Crear). Esto muestra la siguiente pantalla:

Crear mienbro de tipo C mediante el PDM


Esto iniciará el editor incorporado. El código C escogido para implementar la función es el siguiente:

#include                
#include                  
                                   
//---------------------------------
                                   
void EsNumerico(char  *entrada,     
                int   *salida,      
                short *input_ind,  
                short *output_ind, 
                char  sqlstate[6], 
                char  fname[140],  
                char  finst[129],  
                char  msgtext[71]) 
{                                  
    while(*entrada)                
    {                              
         if (!isdigit(*entrada))
         {                      
              *salida=0;        
              return;           
         }                      
         //--                   
         entrada++;             
    }                           
    *salida=1;                  
}


Este puede tomarse tal cual y pegarse en la consola TN5250 y debería compilar sin errores.

En el ejemplo se observa que el programa tiene un único método denominado EsNumerico, el cual recorre la hilera recibida en el parámetro entrada y va comparando cada elemento mediante la función isdigit.

Como se explicó previamente, al utilizar el valor SQL en la cláusula PARAMETER STYLE del CREATE FUNCTION, se debe incluir a la función en C una serie de parámetros adicionales requeridos por el motor de base de datos.  Estos contienen información de control y de invocación. A continuación se detalla la lógica que rige la definición de estos parámetros:


  • Los primeros N parámetros de la función C, corresponden a los N parámetros de la función SQL. En este caso, como la función definida en el CREATE FUNCTION solo tiene un parámetro de tipo VARCHAR, entonces este será recibido en el primer parámetro de la función C que será de tipo *char. 
  • Seguido a los parámetros de entrada, está el parámetro que contendrá el valor de salida (int *salida). Dado que la función SQL se definió como de tipo entera (RETURNS INTEGER), entonces el parámetro en C será de tipo int 
  • Después del parámetro de salida, se deben definir N parámetros de tipo short, donde N está representada por la cantidad de parámetros de la función SQL. Estos parámetros contienen indicadores de entrada.
  • Seguido a estos indicadores de entrada, se deben definir un solo parámetro de tipo short que corresponde a un indicador de salida.
  • Por último, se deben definir las variables sqlstate[6], fname[140],  finst[129] y msgtext[71]. sqlstate sirve para retornar códigos de excepción, fname contiene el nombre de la función SQL que está haciendo el llamado, finst contiene un nombre específico y msgtext permite retornar mensajes de excepción.
Teniendo esto claro, podemos continuar con la compilación de la función.

En los sistemas IBM i, la compilación se puede efectuar con opción 14 para crear un objeto de tipo *PGM u opción 15 para crear uno de tipo *MODULE. La opción a seleccionar va ser la 15, dado que el programa final utilizado por el SQL debe ser de tipo *SRVPGM y estos solo pueden crearse a partir de objetos de tipo *MODULE. Entonces, compilamos el miembro mediante la opción 15, tal y como se muestra en la siguiente imagen:

Creación de un módulo a partir de un miembro en lenguaje C

Si el proceso de compilación resulta exitoso, tendremos en nuestra biblioteca un objeto denominado ESNUMERICO de tipo *MODULE. Como se muestra a continuación:

Objeto de tipo *MODULE resultante de la compilación

Una vez ha sido creado el objeto de tipo *MODULE en la biblioteca, se debe generar a partir de esto un objeto de tipo *SRVPGM. Para esto utilizaremos el comando CRTSRVPGM, tal y como se muestra en la siguiente imagen:

Opciones del comando CRTSRVPGM

Analicemos un poco los parámetros que espera este comando:


  • Programa de servicio: Corresponde al nombre del objeto de salida. Este sera el objeto de tipo *SRVPGM utilizado por el SQL.
    • Biblioteca: Biblioteca donde será creado el objeto.
  • Modulo: Nombre del objeto de tipo *MODULE creado en el paso anterior.
    • Biblioteca: Biblioteca donde se encuentra el objeto de tipo *MODULE
  • Exportar: Especifica que datos y programas serán exportados al objeto *SRVPGM. Colocar la opción *ALL
Los demás parámetros quedan con su valor por defecto.

Al ejecutar el comando se debe generar el siguiente mensaje de salida:

Programa de servicio ESNUMERICO creado en biblioteca SQLUTIL.

Si todos los pasos se han seguido, en este punto la función SQL ya debería funcionar. Para verificarlo, se puede iniciar la consola de comandos SQL (STRSQL) y ejecutar la siguiente sentencia:

SELECT ESNUMERICO('123') FROM SYSIBM/SYSDUMMY1

Antes de esto, hay que cerciorarse que la biblioteca donde se haya definido la función SQL se encuentre en nuestra lista de bibliotecas. Si no lo está, agregarla mediante el comando ADDLIBLE

El resultado debería ser igual al mostrado en la siguiente imagen:

Resultado de la función en la consola SQL 

Hasta aquí esta nota. Pronto un nuevo artículo sobre como depurar el programa C construido.