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!

2 comentarios:

  1. Hola amigo, como te fue con tú error

    ResponderEliminar
  2. Yo recibí el siguiente error: /QIBM/ProdData/OS/WebServices/V1/client/bin/wsdl2ws.sh: 001-0014 Command /QOpenSys/QIBM/ProdData/JavaVM/jdk70/32bit/bin/java not
    found.
    $

    ResponderEliminar