lunes, 3 de marzo de 2014

Introducción al ESB de WSO2 a través de ejemplos prácticos. IV

En entradas anteriores ya hemos tocado el tema del ESB de WSO2 para proxiar un servicio de acceso a datos, agregarle seguridad Username Token y  como desarrollar una arquitectura de seguridad usando el ESB en combinación con el Identity Server de WSO2.

En esta entrada les quiero mostrar cómo hacer un encadenamiento de servicios, Service Chaining, para consultar varios servicios y a partir de la información obtenida retornar una respuesta que integre dichos datos.

De forma general se puede ver la idea en la siguiente figura.






La descripción del escenario es como sigue:

Tenemos en una organización dada, que la información sobre las personas está distribuida en varias aplicaciones. En este caso para simplificar solo usamos 2 aplicaciones con un número reducido de datos sobre las personas.
  • En una aplicación 1 tenemos: nombre y edad.
  • En una aplicación 2 tenemos: nombre, dirección y grado escolar.
  • No se tiene acceso directo a las BD de las aplicaciones, pero se poseen servicios de acceso a datos.

Se quiere brindar un servicio que devuelva el concepto persona con los siguientes datos: nombre, edad, dirección y grado escolar, siempre que se introduzca el nombre de la persona.

Para implementar el escenario se han creado 2 servicios de acceso a datos encargados de extraer la información y un servicio proxy que concatena llamadas a estos servicios para crear una entidad persona como se desea.

Veamos cómo se hace:

Tendremos 2 servicios de acceso a datos que acceden a información de las siguientes tablas:

CREATE TABLE ws.userapp1 (
  id INTEGER NOT NULL,
  nombre VARCHAR NOT NULL,
  edad INTEGER NOT NULL,
  CONSTRAINT "userApp1_pkey" PRIMARY KEY(id)
) WITHOUT OIDS;

COMMENT ON COLUMN ws.userapp1.nombre
IS 'nombre de la persona';

COMMENT ON COLUMN ws.userapp1.edad
IS 'edad de la persona';



CREATE TABLE ws.userapp2 (
  id INTEGER NOT NULL,
  nombre VARCHAR NOT NULL,
  direccion VARCHAR NOT NULL,
  ge VARCHAR NOT NULL,
  CONSTRAINT "userApp2_pkey" PRIMARY KEY(id)
) WITHOUT OIDS;

COMMENT ON COLUMN ws.userapp2.nombre
IS 'nombre  de la persona';

COMMENT ON COLUMN ws.userapp2.direccion
IS 'direccion de la persona';

COMMENT ON COLUMN ws.userapp2.ge
IS 'grado escolar de la persona';

Los pasos para crear  servicios de acceso los pueden encontrar aquí.

Una vez creados los servicios de acceso a datos debemos diseñar el WSDL del servicio  proxy. En este caso sería el siguiente:

NOTA: vean como el nombre del element "getPersona" coincide con el nombre de la operación y dentro del namespace que se encuentra, lo cual es necesario conocerlo para poder extraer la información del mensaje de entrada usando XPATH.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://tutoriales.net/escuela_invierno" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="Persona" targetNamespace="http://tutoriales.net/escuela_invierno">
  <wsdl:types>
    <xsd:schema targetNamespace="http://tutoriales.net/escuela_invierno">
      <xsd:element name="getPersona">
        <xsd:complexType>
          <xsd:sequence>
            <xsd:element name="nombre" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
      <xsd:element name="getPersonaResponse">
        <xsd:complexType>
          <xsd:sequence>
           <xsd:element name="nombre" type="xsd:string" />
           <xsd:element name="direccion" type="xsd:string"></xsd:element>
           <xsd:element name="edad" type="xsd:int"></xsd:element>
           <xsd:element name="ge" type="xsd:string"></xsd:element>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
  </wsdl:types>
  <wsdl:message name="getPersonaRequest">
    <wsdl:part element="tns:getPersona" name="parameters"/>
  </wsdl:message>
  <wsdl:message name="getPersonaResponse">
    <wsdl:part element="tns:getPersonaResponse" name="parameters"/>
  </wsdl:message>
  <wsdl:portType name="Persona">
    <wsdl:operation name="getPersona">
      <wsdl:input message="tns:getPersonaRequest"/>
      <wsdl:output message="tns:getPersonaResponse"/>
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="PersonaSOAP" type="tns:Persona">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="getPersona">
      <soap:operation soapAction="http://tutoriales.net/escuela_invierno/NewOperation"/>
      <wsdl:input>
        <soap:body use="literal"/>
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal"/>
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="Persona">
    <wsdl:port binding="tns:PersonaSOAP" name="PersonaSOAP">
      <soap:address location="http://www.example.org/"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

Como ven recibe un String que sería el nombre de la persona y devuelve un objeto persona con la información que se pide.

A la hora de crear el servicio proxy hemos seguido la buena práctica de guardarlo todo en el registro del ESB por esa razón tanto los endpoint como las secuencias y el mismo WSDL están en el registro. Esto puede parecer un poco complicado ya que primero hay que imaginarse el servicio proxy antes de ponerse a implementarlo pero nos forza a hacerlo bien.

Estos son los endpoint a los 2 servicios de acceso a datos:

<endpoint xmlns="http://ws.apache.org/ns/synapse">
   <address uri="http://localhost:8280/services/userapp1_DataService">
      <suspendOnFailure>
         <progressionFactor>1.0</progressionFactor>
      </suspendOnFailure>
      <markForSuspension>
         <retriesBeforeSuspension>0</retriesBeforeSuspension>
         <retryDelay>0</retryDelay>
      </markForSuspension>
   </address>
</endpoint>

El primer endpoint es para consumir el servicio de datos de la aplicación 1, mientras que el endpoint 2 es para consumir el servicio de la aplicación 2.

<endpoint xmlns="http://ws.apache.org/ns/synapse">
   <address uri="http://localhost:8280/services/userApp2_DataService">
      <suspendOnFailure>
         <progressionFactor>1.0</progressionFactor>
      </suspendOnFailure>
      <markForSuspension>
         <retriesBeforeSuspension>0</retriesBeforeSuspension>
         <retryDelay>0</retryDelay>
      </markForSuspension>
   </address>
</endpoint>


Y estás son las 2 secuencias que se concatenan para extraer la información.

<sequence xmlns="http://ws.apache.org/ns/synapse">
   <property xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://tutoriales.net/escuela_invierno" name="nombre" expression="//p:getPersona/nombre" scope="default" type="STRING"></property>
   <log>
      <property name="SEQ1" value="Accediendo secuencia UNO"></property>
      <property xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://tutoriales.net/escuela_invierno" name="NOMBRE1" expression="get-property('nombre')"></property>
   </log>
   <payloadFactory media-type="xml">
      <format>
         <p:getbyname xmlns:p="http://ws.wso2.org/dataservice">            
            <p:nombre>$1</p:nombre>         
         </p:getbyname>
      </format>
      <args>
         <arg xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://tutoriales.net/escuela_invierno" expression="//p:getPersona/nombre" evaluator="xml"></arg>
      </args>
   </payloadFactory>
   <send receive="conf:/winter/getapp2" buildmessage="true">
      <endpoint key="conf:/winter/endpoint_app1"></endpoint>
   </send>
</sequence>

La primera secuencia  captura en la propiedad "nombre" usando la expresión XPATH siguiente: //p:getPersona/nombre aquí uso p para vincularlo con el namespace http://tutoriales.net/escuela_invierno y de esta manera poder acceder a la información.

Luego uso el mediador log para imprimir los valores que tengo en las propiedades  SEQ1 y NOMBRE1, vean como en este último caso utilizo get-property('nombre') para poner como contenido de NOMBRE1 el contenido de la propiedad nombre, creada más arriba.

El siguiente mediador que se usa es el payloadFactory, para crear un mensaje nuevo de acuerdo con lo que espera el primer servicio de datos y uso el mediador send para enviarlo al endpoint correspondiente. La concatenación ocurre aquí pues en el mediador send le indico que el resultado de esta llamada se enviará a la secuencia que se encuentra en conf:/winter/getapp2 y así es como enlazo una secuencia con la otra.

<sequence xmlns="http://ws.apache.org/ns/synapse">
   <property xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://ws.wso2.org/dataservice" name="edad" expression="//p:Personas/p:Persona/p:edad" scope="default" type="STRING"></property>
   <log>
      <property name="SEQ2" value="Accediendo a la secuencia DOS"></property>
      <property xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://tutoriales.net/escuela_invierno" name="NOMBRE2" expression="get-property('nombre')"></property>
      <property xmlns:ns="http://org.apache.synapse/xsd" name="EDAD2" expression="get-property('edad')"></property>
   </log>
   <payloadFactory media-type="xml">
      <format>
         <p:getbyname xmlns:p="http://ws.wso2.org/dataservice">            
            <p:nombre>$1</p:nombre>         
         </p:getbyname>
      </format>
      <args>
         <arg xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://tutoriales.net/escuela_invierno" expression="get-property('nombre')" evaluator="xml"></arg>
      </args>
   </payloadFactory>
   <send>
      <endpoint key="conf:/winter/endpoint_app2"></endpoint>
   </send>
</sequence>

La secuencia 2 es muy similar a la 1 así que no la comentaré ya que se usan los mismos mediadores, lo que en este caso se consulta al servicio de datos 2 y no se llama a otra sencuencia en el mediador send. Lo que implica que la respuesta que llegue al ESB irá hacia la sencuencia de salida.

Luego viene la secuencia de salida  que se encarga de unir la información:
<sequence xmlns="http://ws.apache.org/ns/synapse">
   <payloadFactory media-type="xml">
      <format>
         <esc:getPersonaResponse xmlns:esc="http://tutoriales.net/escuela_invierno">            
            <nombre xmlns="">$1</nombre>            
            <direccion xmlns="">$2</direccion>            
            <edad xmlns="">$3</edad>            
            <ge xmlns="">$4</ge>         
         </esc:getPersonaResponse>
      </format>
      <args>
         <arg xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://ws.wso2.org/dataservice" expression="get-property('nombre')" evaluator="xml"></arg>
         <arg xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://ws.wso2.org/dataservice" expression="//p:Personas/p:Persona/p:direccion" evaluator="xml"></arg>
         <arg xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://ws.wso2.org/dataservice" expression="get-property('edad')" evaluator="xml"></arg>
         <arg xmlns:ns="http://org.apache.synapse/xsd" xmlns:p="http://ws.wso2.org/dataservice" expression="//p:Personas/p:Persona/p:ge" evaluator="xml"></arg>
      </args>
   </payloadFactory>
   <send></send>
</sequence>

Como se ve en esta sencuencia solo se usa el payloadFactory para ensamblar un mensaje a partir de la información guardada en las propiedades o que viene de la respuesta del servicio de datos 2.

Por último el servicio proxy ya creado.

<?xml version="1.0" encoding="UTF-8"?>
<proxy xmlns="http://ws.apache.org/ns/synapse" name="merge_persona"
       transports="https http"
       startOnLoad="true"
       trace="disable">
   <description/>
   <target faultSequence="fault">
      <inSequence>
         <log>
            <property name="MAIN" value="Accediendo a la secuencia principal"/>
         </log>
         <sequence key="conf:/winter/getapp1"/>
      </inSequence>
      <outSequence>
         <sequence key="conf:/winter/salida"/>
      </outSequence>
   </target>
   <publishWSDL key="conf:/winter/Persona.wsdl"/>
</proxy>

Espero les sea de utilidad.

3 comentarios:

  1. Hola,
    he realizado este ejemplo y no me funciona. He creado los 2 datasources con los métodos estandar y no entiendo cómo la secuencia 1 genera tanto el payload hacia el primer endpoint como el método que tiene que utilizar, ya que por defecto, el datasource utiliza el ID como clave.

    Siguiendo este razonamiento, he creado un método en el UserApp1 para obtener los datos mediante el nombre, llamado getbyname, pero tampoco me consigue capturar la edad.

    Te agradecería que pudieses explicarme esto ya que llevo unos días y no consigo hacerlo funcionar.

    Gracias

    ResponderEliminar
  2. para generar el payload se usa el mediador payloadfactory, lo puedes ver en la secuencia 1 y para determinar el metodo axis2 usa el nombre del elemento que contiene el payload o tambien puede usar el soapaction, que no es este el caso. igual me puedes escribir usando el formulario y te respondo con mas detalle por correo.

    ResponderEliminar
  3. Funciona perfectamente, excelente post.

    ResponderEliminar