Hace no demasiado escribí un larguísimo post hablando sobre un montón de problemas que me habia encontrado al tratar los namespaces con las bibliotecas dom disponibles en python: ElementTree y Minidom.
Ahora traigo unos mucho menos graves y que pudieron ser solucionados en media horita sin grandes dramas.
1º Problema: Los namespaces no se declaran solos…
El problema esta vez era que en mi aplicación compongo un fichero bpts grande con muchos casos de uso tomados de muchos otros ficheros bpts. Esto implica ‘copiar y pegar’ trozos de un xml en otro xml.
Por ejemplo:
<?xml version=”1.0″ encoding=”UTF-8″?>
<!– Fichero base para agrupar todos los casos de prueba –>
<tes:testSuite
xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<!–Nombredelproyecto–>
<tes:name></tes:name>
<!– Servidor y puerto (valores por defecto) –>
<tes:baseURL>http://localhost:7777/ws</tes:baseURL>
<!–Ficherobpr–>
<tes:deployment>
<!–Nombre del proyecto –>
<tes:putname=”bpel_original”type=”activebpel”>
<!– wsdl test –>
<tes:wsdl></tes:wsdl>
<tes:propertyname=”BPRFile”>bpr_file.bpr</tes:property>
</tes:put>
<!–<tes:partnername=”assessor”wsdl=”AssessorService.wsdl”/>–>
</tes:deployment>
<tes:testCases>
</tes:testCases>
</tes:testSuite>
Este es el esqueleto de un fichero test.bpts general, donde se pegan trocitos como estos, que son los casos de prueba:
<tes:testCase name=”LargeAmount” basedOn=”" abstract=”false” vary=”false”>
<tes:clientTrack>
<tes:sendReceive
service=”sp:LoanServiceService”
port=”LoanServicePort”
operation=”grantLoan”>
<tes:sendfault=”false”>
<tes:data>
<esq:ApprovalRequest>
<esq:amount>150000</esq:amount>
</esq:ApprovalRequest>
</tes:data>
</tes:send>
<tes:receivefault=”false”>
<tes:condition>
<tes:expression>esq:ApprovalResponseesq:accept</tes:expression>
<tes:value>’true’</tes:value>
</tes:condition>
</tes:receive>
</tes:sendReceive>
</tes:clientTrack>
(…)
</tes:testCase>
Si os fijais, algunos elementos del caso de prueba tienen un prefijo diferente… porque tienen un namespace diferente, si los copio y pego añadiendo el elemento directamente así:
testCases.appendChild( testCase.cloneNode(true) )
El resultado es lo siguiente:
<?xml version=”1.0″ encoding=”UTF-8″?>
<!– Fichero base para agrupar todos los casos de prueba –>
<tes:testSuite
xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
(…)
<tes:testCases>
<tes:testCasename=”LargeAmount”basedOn=”"abstract=”false”vary=”false”>
<tes:clientTrack>
<tes:sendReceive
service=”sp:LoanServiceService”
port=”LoanServicePort”
operation=”grantLoan”>
<tes:send fault=”false”>
<tes:data>
<esq:ApprovalRequest>
<esq:amount>150000</esq:amount>
</esq:ApprovalRequest>
</tes:data>
</tes:send>
<tes:receivefault=”false”>
<tes:condition>
<tes:expression>esq:ApprovalResponse/esq:accept</tes:expression>
<tes:value>’true’</tes:value>
</tes:condition>
</tes:receive>
</tes:sendReceive>
</tes:clientTrack>
(…)
</tes:testCases>
</tes:testSuite>
¿Notais que falte algo? Si no lo haceis vosotros el parser sí que lo hará. Falta la declaración del namespace con prefijo ‘esq’. Con el prefijo solo NO nos sirve de nada. Para que el documento esté bien formado necesitamos la uri declarada en un atributo xmlns:prefix=”uri”, y minidom no es lo suficientemente inteligente como para mirar los atributos documentURI de los elementos a la hora de serializarlos y tomarse la molestia de declarar los namespaces no declarados. En general, minidom no es inteligente. Pero al menos (a diferencia de ElementTree, que se pasa de listo) de puro tonto nos sirve, ya que nos permite acceder a las uri y prefijos de los namespaces de un documento, y es un buenazo: no elimina los atributos de las declaraciones de namespaces cuando parsea un xml como si que hace ElementTree.
El arreglo más vago existente, es simplemente, preparar el testCase para que contenga las declaraciones de los namespaces ‘inline’ es decir, que cada elemento con namespace, tenga su declaración de namespace, para ahorrarnos disgustos más tarde.
Por ejemplo, el testCase anterior se convertiría en este otro:
<tes:testCase abstract=”false” basedOn=”"
name=”LoanApprovalProcess.bpts:LargeAmount”
vary=”false”xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<tes:clientTrackxmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<tes:sendReceiveoperation=”grantLoan” port=”LoanServicePort” service=”sp:LoanServiceService” xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<tes:sendfault=”false” xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<tes:data xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<esq:ApprovalRequest xmlns:esq=”http://xml.netbeans.org/schema/Loans”>
<esq:amount xmlns:esq=”http://xml.netbeans.org/schema/Loans”>150000</esq:amount>
</esq:ApprovalRequest>
</tes:data>
</tes:send>
<tes:receivefault=”false” xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<tes:condition xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>
<tes:expression xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>esq:ApprovalResponse/esq:accept</tes:expression>
<tes:value xmlns:tes=”http://www.bpelunit.org/schema/testSuite”>’true’</tes:value>
</tes:condition>
</tes:receive>
</tes:sendReceive>
</tes:clientTrack>
(…)
</tes:testCase>
¿Como podríamos conseguir esto? Simplemente recorriendo el árbol o la rama del testCase que queremos incluir y añadiéndo las declaraciones ‘a pelo’:
def minidom_namespaces(elto):
”"”@briefDeclara inline namespaces no declarados
@paramelto Elemento padre”"”
#Utilizamosunacola y procesamos los elementos en orden de documento
eltos= [elto]
while eltos :
e= eltos.pop(0)
#Les añadimos la declaración del namespace si tienen
if e.namespaceURI:
e.setAttribute(‘xmlns:’+ e.prefix, e.namespaceURI)
#Metemos sus hijos en la cola
eltos.extend(e.childNodes)
returnelto
Esta pequeña función toma un elemento y lo recorre junto a sus hijos empleando una cola.
Para cada nodo, si tiene namespace, le añadimos un atributo con la declaración del mismo.
El código xml al final es BASTANTE feo, por ser demasiado prolijo y redundante, pero total, a quien le importa, es código temporal que utiliza el proyecto y que el usuario no va a ver.
2º Problema: Aún quedan namespaces sin declarar… pero están escondios
Pero el anterior no fué el único problema que me encontré.
Echadle un vistazo otra vez el caso y mirad otros namespaces que no son tes:
<tes:testCase name=”LargeAmount” basedOn=”" abstract=”false” vary=”false”>
<tes:clientTrack>
<tes:sendReceive
service=”sp:LoanServiceService”
port=”LoanServicePort”
operation=”grantLoan”>
<tes:sendfault=”false”>
<tes:data>
<esq:ApprovalRequest>
<esq:amount>150000</esq:amount>
</esq:ApprovalRequest>
</tes:data>
</tes:send>
<tes:receivefault=”false”>
<tes:condition>
<tes:expression>esq:ApprovalResponse/esq:accept</tes:expression>
<tes:value>’true’</tes:value>
</tes:condition>
</tes:receive>
</tes:sendReceive>
</tes:clientTrack>
(…)
</tes:testCase>
¿Veis a lo que me refiero?
Hay un namespace DENTRO de un campo de contenido, de texto… ¡otra vez! suspiro.
Es el mismo problema que habia encontrado al tratar con los wsdl, pero ahora mucho más complicado, ya que los namespaces del caso de uso son completamente impredecibles, puede haber muchos y además… ¡puede hasta que se pisen unos a otros con los prefijos!
Aquí la solución ha pasado por una decisión podríamos llamarla ‘de diseño’.
“”"Todas las declaraciones de namespaces que se usen dentro de un caso de uso, pero que no tengan ningún elemento real con ese namespace dentro del caso, deben estar declarados en el elemento testSuite, que es el primero del bpts”"”"
Esto no es demasiado drástico, ya que al ser testSuite el primer elemento del documento, lo normal es que los namespaces estén declarados ahí, pero ya es poner ciertas limitaciones y dejar la puerta abierta a posibles errores cometidos por el usuario. Una tarea que tengo pendiente sería testar todo esto con los namespaces mal puestos y manejar los errores de manera que advierta de todo esto al usuario en la interfaz gráfica.
La solución la implementé de la siguiente manera:
# Añadimos al testSuite de test.bpts las declaraciones
# de espacios de nombres del .bpts nuevo
for prefix, uri in testSuite.attributes.items() :
ifnotttestSuite.hasAttribute(prefix):
ttestSuite.setAttribute(prefix,uri)
Este trozo de código se encuentra en la función idg.proyecto.add_bpts_info, que es la encargada de establecer la configuración del test.bpts general, y se llama cada vez que se añade un nuevo bpts al proyecto. Lo que hace es copiar las declaraciones de namespaces del nuevo, al test.bpts general.
minidom mantiene los atributos en un tipo propio de diccionario llamado NameNodeMap. Recorremos así los atributos que tiene el testSuite del bpts a importar y los ponemos en ttestSuite que es el elemento del test.bpts general.
3º Problema: Vamos a ver, Minidom, ¡ya te he dicho que lleva namespace!
Ooootro pequeño (diminuto pero puñetero) problema que me he encontrado relacionado con este tema es la creación de nuevos elementos en un documento empleando un namespace existente.
Uno esperaría que si tiene este sencillo ejemplo:
import xml.dom.minidom as md
foo = md.parseString(‘<foo:first xmlns:foo=”www.foo.com”/>’)
new = md.createElementNS(‘www.foo.com’, ‘new’)
foo.firstChild.appendChild(new)
print foo.toxml()
El resultado fuese este:
<foo:first xmlns:foo=”www.foo.com”>
<foo:new/>
</foo:first>
Pero la triste realidad es que el código de arriba genera:
<foo:first xmlns:foo=”www.foo.com”>
<new/>
</foo:first>
¿WTF? ¿No lo he creado expresamente con createElementNS? Una pequeña exploración en el código anterior te diría que sí, que el elemento new, creado con createElementNS, tiene el atributo new.namespaceURI… pero no se serializa el prefijo correspondiente… simplemente porque no se lo has puesto. Como ya he dicho antes, minidom es bastante poco inteligente a la hora de tratar con namespaces, su único mérito es dejarnos la información con libre acceso para que al menos podamos nosotros manejarla.
En este caso lo único que deberíamos hacer es ponerle el prefijo manualmente al elemento:
new = md.createElementNS(‘www.foo.com’, ‘foo:new’)
Esto es, a mi modo de ver, una chapuza. Porque imaginemos (como es el caso) que estoy editando un fichero que tiene ya puesto el namespace www.foo.com… ¡pero que yo no sé su prefijo! ¡El prefijo podría ser cualquier cadena aleatoria! En la situación en la que resolví el problema, es que sabia que iba a ser siempre cierta cadena, porque está hardcodeada en xpaths de otro componente de la aplicación, pero en otra situación el no conocerlo me obligaría a repasar el documento, o a buscar la declaración del namespace y sacar el prefijo, manualmente… dolorosamente.
Y bueno, así, lidiando con los namespaces, se me van las horas. Vamos, que estoy entretenido.