Este es un post sobre los dolores de cabeza y el amargo dolor del tiempo perdido.
Como comenté en el post anterior, se decidió tomar el bpel que nos daba un usuario e importarlo, buscando sus dependencias, dentro del proyecto. Para así tratar con el de la manera que más nos apeteciese.
Este proceso es el de búsqueda recursiva de dependencias (.bpel, .wsdl, .xsd) de un bpel dado y encuentra todos los imports que tenga, y todos los imports del fichero que importa…
El procedimiento para hacer esto sería el siguiente:
1. Abrir el fichero F con un parser xml
2. Buscar todos los import a ficheros externos (f1, f2, ..)
4. Copiar el fichero F al proyecto pero modificando la ruta que ponia en el import a la que tendrá dentro
del proyecto (típicamente ./dependencias/fichero)
5. Volver al paso 1 con todos los nuevos ficheros encontrados (f1, f2, ..) y buscar imports…
Una simplificación de un caso a tratar sería este bpel, que importa dos wsdl y estos un xsd:
El código para hacer el grafo dot seria algo así:
digraph F {
FicheroFuente_bpel -> ApprovalService_wsdl -> Loans_xsd
FicheroFuente_bpel -> AssessorService_wsdl -> Loans_xsd
}
Y aquí el código para que podais ver la relación.
$cat fichero_bpel.bpel
<process name=’”LoanApprovalProcess”’ xmlns=’”http://docs.oasis-open.org/wsbpel/2.0/process/executable”’ (muuuuchos namespaces)>
<import namespace=’”http://j2ee.netbeans.org/wsdl/ApprovalService”’ location=’”ApprovalService.wsdl”’ importtype=’”http://schemas.xmlsoap.org/wsdl/”/’>
<import namespace=’”http://j2ee.netbeans.org/wsdl/AssessorService”’ location=’”AssessorService.wsdl”’ importtype=’”http://schemas.xmlsoap.org/wsdl/”/’>
</process>
$cat ApprovalServide.wsdl
<definitions name=”ApprovalService” xmlns=”http://schemas.xmlsoap.org/wsdl/” (muuuuchos namespaces)>
<types>
<xsd:schema targetNamespace=”http://j2ee.netbeans.org/wsdl/ApprovalService”>
<xsd:import namespace=”http://xml.netbeans.org/schema/Loans”schemaLocation=”Loans.xsd”/>
</xsd:schema>
</types>
</definitions>
$cat AssessorService.wsdl
(…)
$cat Loans.xsd
(Loans no importa nada)
Bien, en realidad el problema no está en parsear todo esto y buscar las dependencias, que es realmente sencillo. Se toma el fichero, se parsea con un módulo de python para xml… ¿Pero cual? Tras una primera documentación, ElementTree me pareció el más completo y sobre todo el más eficiente. Además es muy ‘pythónico’ siendo muy agradable de utilizar.
Básicamente mediante ElementTree podíamos tomar un fichero, cargarlo en memoria representado mediante una estructura en árbol, modificarlo tranquilamente y volverlo a escribir en xml (serializarlo que se dice). Todo muy sencillo, y sin embargo… ¿Habeis visto la presencia de namespaces en el código anterior? Fijaos bien.
Los namespaces se utilizan para resolver entre otros, problemas de colisiones de nombres, son una medida que puede dar quebraderos de cabeza y más de una vez he leido a gente que los evita siempre que puede. El por qué me dan miedo los namespaces lo explico a hora.
Todo esto viene debido a que el estandar XML dice que un namespace es básicamente una url que se asocia a cada elemento, pero como no podemos añadir un chorizo enorme delante de cada elemento en el xml por motivos de espacio y legibilidad, se usará un prefijo. Y ese prefijo, según el estandar, puede ser cualquier cadena aleatoria, no tiene significado ninguno.
Vamos a tomar un fragmento omitido del bpel anterior como referencia:
$cat fichero_bpel.bpel
<process xmlns=”http://docs.oasis-open.org/wsbpel/2.0/process/executable” xmlns:ns0=”http://xml.netbeans.org/schema/Loans” (…) >
<if name=”If1″>
<condition> ( number(string($processInput.input/ns0:amount)) <= 10000 ) </condition>
<sequence name=”SmallAmount”>
<assign name=”copyLoanInfoToAssessorInput”>
<copy>
<from>$processInput.input/ns0:amount</from>
<to>$assessorInput.input/ns0:amount</to>
</copy>
(…)
Este sería el código original y como puede verse tenemos elementos ns0:amount… dentro del texto de un elemento.
A lo que me enfrenté fué que ElementTree, como buena seguidora del estandar, asigna prefijos inventados a los namespaces al serializar un documento, esto es, al escribirlo en xml genera los prefijos como le da la gana. Si por ejemplo parseamos el xml de arriba y lo serializamos acto seguido, lo que nos queda es lo siguiente:
$cat fichero_bpel_serializado.bpel
<ns0:process xmlns:ns0=”http://docs.oasis-open.org/wsbpel/2.0/process/executable” xmlns:ns1=”http://xml.netbeans.org/schema/Loans” (…)>
<ns0:if name=”If1″>
<ns0:condition> ( number(string($processInput.input/ns0:amount)) <= 10000 ) </condition>
<ns0:sequence name=”SmallAmount”>
<ns0:assign name=”copyLoanInfoToAssessorInput”>
<ns0:copy>
<ns0:from>$processInput.input/ns0:amount</from>
<ns0:to>$assessorInput.input/ns0:amount</to>
</copy>
(…)
Como se puede ver, los prefijos de los namespaces los reinventa. Esto provoca que cualquier operación con el bpel posterior en la que intervengan xpath con prefijo ‘fijo’ provoque errores por todas partes y genere auténticos quebraderos de cabeza.
El problema viene cuando se aplica a un xml una consulta xpath las cuales se basan en el prefijo para manejar namespaces. Esto es importante, ya que, si siguiendo el estandar, ponemos prefijos aleatorios, al aplicar el instrumentador, las xpath con prefijos ya puestos… no van a funcionar. De ahí que mucha gente le coja cariño a sus namespaces y pretenda mantenerlos.
Pero sin embargo, hay otro problema más. ¡Mirad dentro del texto de los elementos!, hay variables que emplean el namespace y que están dentro del texto de los elementos. Esto lo que hace es mezclar la estructura del documento, con el contenido, xml no está pensado para esto y no se espera en ningún momento que aparezca un namespace en un campo de texto. Al serializarse y cambiar todos los namespaces de sitio, el ns0 pasa a ser ns1 pero dentro del texto sigue siendo ns0 y significa otra cosa, entonces obviamente ya nada de esto funciona.
En ese momento me embarqué en una búsqueda sobre como mantener los prefijos de los namespaces durante la serialización con ElementTree, y comencé a hacer pequeños intentos de chapuza, como a recorrer todo el documento cambiando tags, a acceder directamente a la implementación del módulo, a un diccionario que mantiene una tabla de los prefijos y los namespaces y modificarlos por los que yo quisiese…
Podía hacer esto último de manera medianamente satisfactoria, sin embargo no podia hacerlo dinámicamente, no podía abrir un documento con ElementTree y leer sus prefijos originales, ya que estos se perdían al cargarse en memoria, todo lo que podia hacer era modificar prefijos de namespaces conocidos, y por supuesto esto no me servía, porque a saber que namespaces metía el usuario en su bpel.
Cosas poco portables y que darían confictos entre versiones, que oscurecian el código y complicaban una operación sencilla.
La solución, después de dar más vueltas que una peonza, de ensuciarme las manos, de errores que desde mi punto de vista no tenian sentido (¡el xml estaba bien!) fué emplear el módulo minidom, más antiguo, feucho y menos eficiente que ElementTree, con un soporte de namespaces discutible, pero que sin embargo respeta los namespaces del documento original y permite al usuario especificar los suyos.
La diferencia radica en que ElementTree añade al tag de los elementos, la uri directamente, y se olvida de los prefijos (como debe ser según el estandar), y sin embargo, Minidom mantiene los prefijos.
Por ejemplo:
$cat test.xml:
<ns0:test xmlns:ns0=”http://wwww.pepe-perez.com/”/>
Se carga en memoria mediante ElementTree como:
>> from xml.etree import ElementTree as et
>> tree = et.ElementTree()
>> tree.parse(‘test.xml’)
<Element {http://www.pepe-perez.com/}test at 7ff6c2da7128>
Se carga en memoria mediante minidom como:
>> from xml.dom import minidom as md
>> tree = md.parse(‘test.xml’)
>> tree.firstChild
<DOM Element: ns0:test at 0x1f4e680>
Utilizo minidom entonces, únicamente para tratar con estos ficheros en los que los prefijos de los namespaces importan, para el resto empleo ElementTree, la cual es todo un placer usar.
Espero que quizás esto sirva a alguien, ya sea a modo de aviso o si lo encuentra por casualidad buscando ayuda relacionada, yo he aprendido por la via dura. Durante el proceso me hizo bastante gracia leer en las listas de correo de desarrolladores de Python al mantenedor de ElementTree negandose a implementar los prefijos personalizables para los espacios de nombres ya que su parser seguia íntegramente el estandar xml…
Aunque pueda parecer una solución muy obvia, en un inicio para mi no lo era tanto ya que minidom, debido a su consumo de memoria, inicialmente no era una opción y no me sería agradable emplearlo a lo largo de todo el proyecto.
Pero bueno este tipo de cosas son las que le hacen sentirse a uno santisfecho cuando se solucionan… huele a victoria
