Fullmenu null

 

26 October 2017

Qué es SOAP

SOAP (originalmente las siglas de Simple Object Access Protocol) es un protocolo estándar que define cómo dos objetos en diferentes procesos pueden comunicarse por medio de intercambio de datos XML. Este protocolo deriva de un protocolo creado por Dave Winer en 1998, llamado XML-RPC. SOAP fue creado por Microsoft, IBM y otros. Está actualmente bajo el auspicio de la W3C. Es uno de los protocolos utilizados en los servicios Web.

— Wikipedia
https://es.wikipedia.org/wiki/Simple_Object_Access_Protocol
Si como yo, siempre has creido que la S no era de Simple sino de Sufre, y la P de Pánico, este es tu post.

En este script vamos a explicar como consumir un servicio SOAP ofrecido por la Agencia Tributaria Española para consultas de calidad de datos identificativos, es decir, validar si un NIF corresponde a la persona que dice ser. Mediante este WebServices podemos enviar una lista de NIFs junto con el nombre del titular y la respuesta nos indicará para cada uno de ellos si según sus registros existe una correspondencia total, parcial o no existe. De esta forma podemos mejorar la calidad de los datos de nuestros clientes por ejemplo.

Para completar el script haremos que la lista de NIFs/Nombres a consultar se tome de una base de datos que será actualizada con la información de respuesta. Así pues deberemos contar con una tabla nifes con la siguiente estructura:

Field Type Description

nif

varchar(10)

NIF proporcionado por el usuario

nombre

varchar(200)

Apellido y nombre proporcionado por el usuario

estado

varchar(10)

INCORRECTO / IDENTIFICADO / PARCIALMENTE-IDENTIFICADO

nombre_aeat

varchar(200)

Apellidos y Nombre proporcionado por AEAT

Así pues el script primeramente creará una petición con los NIF/Nombres en estado Incorrecto (o nulo) y parseará la respuesta actualizándolos con los datos obtenidos de la AEAT

SOAP y Java

Si nunca has tenido que consumir un servicio SOAP puedes echarle un ojo a este artículo https://docs.oracle.com/javaee/5/tutorial/doc/bnayn.html de Oracle donde en apariencia no es tan difícil de hacer, sobre todo gracias a las anotaciones que existen hoy en día.

Básicamente SOAP es una forma de consumir un servicio remoto vía HTTP a través de intercambios de mensajes XML (y qué mensajes!!) Al ser HTTP no hace falta exponer puertos especiales en nuestros sistemas (o si los abrimos podremos tratarlos como cualquier puerto que atienda este protocolo), así como aprovechar todo el stack de seguridad como puede ser el uso de SSL.

Por una parte diseñas el interface a exponer junto con sus parámetros tanto de entrada como de salida y con la ayuda del veneno que prefieras (Axis, Spring, CXF, …​) generas un punto de entrada al mismo.

Con ayuda de tu veneno expones también el WSDL que es lo que necesitará cualquier programa cliente que quiera consumirlo (como puedes imaginar, el XML no está pensado para los ojos humanos, al menos no para los mios) usando a su vez su propio veneno

wsimport y wsdl2java son sólo algunos de los venenos que puedes elegir para ayudarte a generar la parte cliente y que de forma genérica te solicitarán una URL donde resida el WSDL (suele ser un servidor web o también una ruta a un fichero si te lo has descargado). Con esta definición tu veneno será capaz de crearte una clases "esqueleto" para que las incluyas en tu proyecto y puedas así invocar al servicio de forma transparente.

SOAP y Groovy

Groovy cuenta con el proyecto groovy-wslite el cual hace realmente simple el consumir un servicio SOAP.

Como avisa el README del proyecto, esta librería asume que conoces el servicio que vas a consumir. Es decir, necesitas saber el "nombre" del método que quieres ejecutar así como sus parámetros. En mi caso esto ha ocurrido en el 99.9999% de las veces, independiente de que un veneno me generara el stub

Mediante esta librería tendremos control absoluto tanto de la Request como de la Response, así como de todos los atributos que se necesiten enviar. Incluso en el peor de los casos, puedes construirte el XML mediante un String y enviarlo directamente.

La idea principal es que modelaremos nuestro intercambio mediante closures y la librería generará los mensajes al vuelo, sin necesidad de una fase inicial de convertir el WSDL a código:

def client = new SOAPClient('http://www.holidaywebservice.com/Holidays/US/Dates/USHolidayDates.asmx') //(1)
def response = client.send() {
    body {
        GetMothersDay('xmlns':'http://www.27seconds.com/Holidays/US/Dates/') { //(2)
            year(2011)  //(3)
        }
    }
}
  1. Construimos un cliente SOAP indicando la ruta al servicio

  2. Invocamos la acción GetMothersDay pudiendo incluso cualificarla con su namespace

  3. Pasamos un parámetro que nos pide el método llamado year

Así mismo la respuesta la podemos analizar sin necesidad de ningún stub:

println "${response.GetMothersDayResponse?.GetMothersDayResult}"  //(1)
  1. Vamos "navegando" por la estructura retornada

Consulta de NIF válido según AEAT

Saber cuándo es el día de la madre de un año cualquiera está muy bien pero con poca utilidad, así que vamos a desarrollar un pequeño script que nos valide si un NIF es de la persona que dice ser. Para ello la Agencia Tributaria ofrece un servicio SOAP de consulta (hasta 10K NIF en una sóla petición!!) donde nos confirma si un NIF corresponde con un nombre en base a sus registros.

Puedes encontrar la definición de este servicio en Información sobre Web Services de Calidad de Datos Identificativos

Para poder usar este servicio necesitarás un certificado digital que te identifique. Puede ser de persona física, empleado público, FNMT o empresas. Según entiendo se requiere simplemente para evitar el abuso del servicio

A continuación un ejemplo de consulta y su respuesta extraidas del documento

Ejemplo de consulta
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:vnif="http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit
/ws/VNifV2Ent.xsd"> (1)
<soapenv:Header/>
<soapenv:Body>
<vnif:VNifV2Ent> (2)
<vnif:Contribuyente>  (3)
<vnif:Nif>99999999R</vnif:Nif>  (4)
<vnif:Nombre>ESPAÑOL ESPAÑOL JUAN</vnif:Nombre>
</vnif:Contribuyente>
</vnif:VNifV2Ent>
</soapenv:Body>
</soapenv:Envelope>
  1. Schema "vnif" que hay que utilizar.

  2. Usando el schema "vnif" identificamos la funcion a ejecutar

  3. Usando el schema "vnif" describimos una estructura de entrada

  4. Usando el schema "vnif" indicamos un parametro

Ejemplo de respuesta
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <env:Body>
 <VNifV2Sal:VNifV2Sal
xmlns:VNifV2Sal="http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNifV2Sal.xsd"> (1)
 <VNifV2Sal:Contribuyente> (2)
 <VNifV2Sal:Nif>99999999R</VNifV2Sal:Nif>
 <VNifV2Sal:Nombre>ESPAÑOL ESPAÑOL JUAN</VNifV2Sal:Nombre>
 <VNifV2Sal:Resultado>Identificado</VNifV2Sal:Resultado>
 </VNifV2Sal:Contribuyente>
 </VNifV2Sal:VNifV2Sal>
 </env:Body>
</env:Envelope>
  1. Schema "VNIFV2Sal" a utilizar

  2. Estructura de respuesta

Consultar un NIF

Para invocar ese servicio un script simple sería:

@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP')
def response = client.send() {
     envelopeAttributes([
            "xmlns:vnif": "http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNifV2Ent.xsd"
     ])
    body {
        'vnif:VNifV2Ent' {
            'vnif:Contribuyente'{
                'vnif:Nif'(0123456789X')
                'vnif:Nombre'('APELLIDO APELLIDO NOMBRE')
            }
        }
    }
}
println "${response.VNifV2Sal.Contribuyente.Resultado}"

Consultar hasta 10K NIFs

Partiendo del ejemplo anterior donde consultamos un NIF vamos a desarrollar un script más complejo donde la petición se construye dinámicamente en función de los valores de la base de datos. Así mismo trataremos una respuesta iterando por cada elemento de interés.

dependencias
@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
@Grab('mysql:mysql-connector-java:5.1.6')
@GrabConfig(systemClassLoader=true)
crear SOAP Client
	def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP') //(1)
  1. Proporcionamos directamente la URL al servicio

crear cabecera
	def response = client.send() {
	     envelopeAttributes([
                "xmlns:vnif": "http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNifV2Ent.xsd" //(1)
		        ])
  1. Podemos ajustar la petición, incluyendo namespaces por ejemplo

body dinámico y con un bucle
	    body {

		sql.eachRow("select * from nifes where estado is null or estado <> 'IDENTIFICADO' "){ row->	//(1)
			'vnif:VNifV2Ent' {	//(2)
			    'vnif:Contribuyente'{	//(3)
					'vnif:Nif'(row.nif)
					'vnif:Nombre'(row.nombre.toUpperCase())
			    }
			}
		}
  1. Recorremos la tabla y construimos elementos dinámicamente

  2. Cualificamos la función según el schema que corresponda si es necesario

  3. Cualificamos los parametros según el schema que corresponda si es necesario

tratamiento respuesta dinámico
	sql.withBatch( batchSize, "update people set nombre_aeat=?,estado=? where nif=?"){ ps->
		response.VNifV2Sal.Contribuyente.each{ result ->	//(1)
			ps.addBatch([
					"$result.Nombre".toString(),	//(2)
					"$result.Resultado".toString(),
					"$result.Nif".toString()
			])
			erroneos+= "$result.Resultado".equals('IDENTIFICADO') ? 0 : 1
		}
	}
  1. response.VNifV2Sal.Contribuyente nos devuelve un array de elementos Contribuyente

  2. Obtenemos el valor de un elemento mediante .text() o con .toString()

excepciones
} catch (SOAPFaultException sfe) { //(1)
    println sfe.request?.contentAsString
    println sfe.request.contentAsString
} catch (SOAPClientException sce) { //(2)
    // This indicates an error with underlying HTTP Client (i.e., 404 Not Found)
    println sce.request?.contentAsString
    println sce.response?.contentAsString
}
  1. Excepción de "negocio"

  2. Excepción de "transporte"

Ejecución y certificado

Como ya se ha comentado el servicio de la AEAT requiere que la conexión sea realizada con un certificado por parte del cliente que sirva para identificarle. La forma más común es tener este certificado en un fichero .p12 protegido con contraseña de tal forma que al ejecutar el script podamos indicarle a Groovy (en realidad a Java) donde encontrarlo.

Así pues la ejecución del script sería algo parecida a:

groovy -Djavax.net.ssl.keyStore=./test.p12 -Djavax.net.ssl.keyStorePassword=LAPWD -Djavax.net.ssl.keyStoreTYpe=PKCS12 ConsultaNif.groovy USER PWD //(1)
  1. LAPWD correspondería a la password para abrir el keystore mientras que USER Y PWD corresponderían al usuario de la base de datos


Script
//tag::dependencies[]
@Grab('com.github.groovy-wslite:groovy-wslite:1.1.2')
@Grab('mysql:mysql-connector-java:5.1.6')
@GrabConfig(systemClassLoader=true)
//end::dependencies[]

import groovy.sql.Sql
import wslite.soap.*
import groovy.xml.*

try{
	sql=Sql.newInstance("jdbc:mysql://localhost/mydatabase",args[0], args[1], "com.mysql.jdbc.Driver")


	//tag::cliente[]
	def client = new SOAPClient('https://www1.agenciatributaria.gob.es/wlpl/BURT-JDIT/ws/VNifV2SOAP') //(1)
	//end::cliente[]

	//tag::cabecera[]
	def response = client.send() {
	     envelopeAttributes([
                "xmlns:vnif": "http://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/burt/jdit/ws/VNifV2Ent.xsd" //(1)
		        ])
	//end::cabecera[]
	//tag::body[]
	    body {

		sql.eachRow("select * from nifes where estado is null or estado <> 'IDENTIFICADO' "){ row->	//(1)
			'vnif:VNifV2Ent' {	//(2)
			    'vnif:Contribuyente'{	//(3)
					'vnif:Nif'(row.nif)
					'vnif:Nombre'(row.nombre.toUpperCase())
			    }
			}
		}
	//end::body[]
	    }
	}

	batchSize=20
    erroneos=0

	//tag::respuesta[]
	sql.withBatch( batchSize, "update people set nombre_aeat=?,estado=? where nif=?"){ ps->
		response.VNifV2Sal.Contribuyente.each{ result ->	//(1)
			ps.addBatch([
					"$result.Nombre".toString(),	//(2)
					"$result.Resultado".toString(),
					"$result.Nif".toString()
			])
			erroneos+= "$result.Resultado".equals('IDENTIFICADO') ? 0 : 1
		}
	}
	//end::respuesta[]
	println "Hay $erroneos no identificados o identificados parcialmente"

	//tag::exceptions[]
} catch (SOAPFaultException sfe) { //(1)
    println sfe.request?.contentAsString
    println sfe.request.contentAsString
} catch (SOAPClientException sce) { //(2)
    // This indicates an error with underlying HTTP Client (i.e., 404 Not Found)
    println sce.request?.contentAsString
    println sce.response?.contentAsString
}
	//end::exceptions[]