Welcome to Comunidad .NET de Cd. Juárez Sign in | Join | Help

September 2008 - Posts

Cómo transformar XML sin usar XSLT

El otro día, una compañía externa hizo un una auditoría de seguridad a un sistema grande que manejamos.  Parte de la auditoría consistió en correr la herramienta FxCop contra las DLLs para buscar—entre otras cosas—posibles vulnerabilidades de inyección de SQL.  FxCop produce dichos reportes en XML y eso fue lo que nos entregaron. 

Estos reportes eran enormes y en un formato no muy amigable, así que necesitábamos de alguna forma extraer los datos de ahí para enfocar los esfuerzos de los desarrolladores (qué corregir y en dónde), y también para tabular los datos (número de issues por namespace, DLL, etc.) de manera que se les pudieran presentar a los gerentoides. 

Este es un ejemplo de uno de esos archivos—no te preocupes, todas estas vulnerabilidades ya fueron parchadas, este es un extracto de esos reportes smile_wink.

(Da clic a la imagen para ver un tamaño más grande)

ejemplo de reporte FxCop

Ahora, se me ocurrió que estos archivos se podían transformar a algo menos plano—como otro archivo de XML o un CSV—de manera que pudiera ser importado a Excel fácilmente.  Algo así como lo siguiente:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Vulnerabilities>
  <Vulnerability>
    <Module></Module>
    <Namespace></Namespace>
    <Type></Type>
    <Method></Method>
    <Issue></Issue>
  </Vulnerability>
  <Vulnerability>
    <Module></Module>
    <Namespace></Namespace>
    <Type></Type>
    <Method></Method>
    <Issue></Issue>
  </Vulnerability>
</Vulnerabilities>

La sola idea de hacer una hoja de estilos XSL para transformar eso me daba escalofríos, así que fue una buena oportunidad para practicar mi LINQ-Fu.  Para ello me ayudé de LINQPad—una excelente herramienta para probar snippets de LINQ.

Armando el query

Mi primer query fue este:

XElement xml = XElement.Load(@"C:\Users\Carlos\Desktop\sampleFxCopReport.xml");
 
var query =
    from e in xml.Descendants("Issue")
    where e.Ancestors("Message").Attributes("TypeName").First().Value == "ReviewSqlQueriesForSecurityVulnerabilities"
    select new
    {
        Module = e.Ancestors("Module").Attributes("Name").First().Value,
        Namespace = e.Ancestors("Namespace").Attributes("Name").First().Value,
        Type = e.Ancestors("Type").Attributes("Name").First().Value,
        Method = e.Ancestors("Member").Attributes("Name").First().Value,
        Issue = e.Value
    };

Como puedes ver, cargar el archivo es trivial.  Puedes hacerlo a través del método Load() de la clase XElement o XDocument.

Luego, declaré una variable de tipo var porque no estaba seguro exactamente del tipo de los objetos que regresaría mi query—utilicé un tipo anónimo, el cual explicaré en un minuto.

Si no estás muy familiarizado con queries de LINQ, solo recuerda que en su nivel más básico, tienes un from/where/select

  • Primero le dices a qué objetos quieres hacerle query (from),
  • luego especificas un filtro que deje solo los que te interesen (where) y
  • finalmente le indicas una expresión de cómo quieres proyectar tus datos en la salida (select).

En este caso, se me hizo más fácil elegir primero todos los elementos <Issue> dentro del documento XML, ya que a partir de ellos puedo obtener el resto de la información viendo los nodos ancestros.  Logré hacer esto con una sola línea en el from gracias al método Descendants() de la clase XElement.  En otras palabras estoy diciendo “selecciona los elementos <Issue> en el documento, y por cada uno de ellos dame una variable e de donde extraer más información”.  Puedes pensarlo como algo parecido a un foreach.

Luego, indiqué en el filtro where que únicamente deseo los <Issue> donde el mensaje (<Message>) sea de tipo vulnerabilidad de SQL (TypeName=”ReviewSqlQueriesForSecurityVulnerabilities”).  Como los elementos <Message> son ancestros de <Issue>, utilicé Ancestors() para acceder a ese elemento y luego Attributes() para obtener el valor de atributo TypeName y poder hacer la comparación.

Finalmente, en la proyección el select new crea un tipo (una clase pues) sin nombre con 5 propiedades, que son inicializadas con los valores de las expresiones.  Esto crea una collección de tipo IEnumerable<T> donde T es el tipo anónimo—¿ves por qué fue necesaria la variable var?

El resultado del query es este (da clic para ver la imagen más grande):

resultados del primer query

Pero, ¿qué tal si quisiera ordenar mis resultados primero por Module, luego por Namespace, Type y Method sucesivamente?  Sencillo.  Puedo agregar una cadena de OrderBy’s a mi query original o puedo agarrar el resultado del query y ordenarlo así:

var ordered =
    query.OrderBy(x => x.Method).OrderBy(x => x.Type)
    .OrderBy(x => x.Namespace).OrderBy(x => x.Module);

Esta manera de hacerlo involucra las famosas expresiones lamda, pero no me quiero desviar en explicarlas.  Por ahora sólo tómalas como ejemplo y nota que puedes encadenar las cláusulas.

Esto es un buen comienzo, pero ahora quiero esos datos en algo que se le parezca a un documento XML.

Entonces agregué esto después de mi query:

XDocument xdoc = new XDocument();
xdoc.AddFirst(new XElement("Vulnerabilities"));
 
foreach (var element in query)
{
    xdoc.Root.Add(
        new XElement("Vulnerability", new XElement("Module", element.Module)
                                    , new XElement("Namespace", element.Namespace)
                                    , new XElement("Type", element.Type)
                                    , new XElement("Method", element.Method)
                                    , new XElement("Issue", element.Issue)
                                    ));
}

Primero creo un nuevo documento XML utilizando la clase XDocument, y le agrego un elemento raíz <Vulnerabilities>.  Luego por cada elemento de mi lista ordenada, agrego un elemento <Vulnerability> que contendrá cinco elementos, uno por cada propiedad que me interesó, y que tendrán el valo.  Nota que los constructores “raros” de XElement facilitan enormemente la tarea, y que los métodos AddFirst() y AddNew() se encargará de cerrar los elementos adecuadamente—a diferencia de la forma en que se utiliza un XmlWriter, por ejemplo.

El resultado:

resultados del query

Esto ya es prácticamente lo que quiero.  Sin embargo, mi código hasta ahorita son como 30 líneas—principalmente por el formato, en realidad solo he utilizado como 7 instrucciones—y me gustaría condensar el código más.  Además descubrí que el ordenamiento en realidad no lo necesito—puedo hacerlo en Excel—y que al documento XML le falta la declaración <?xml> al pricipio.

Sintetizando el código

Así que decidí cambiar la proyección (el select pues) de mi query original.  El total del código quedó así:

XElement xml = XElement.Load(@"C:\Users\Carlos\Desktop\sampleFxCopReport.xml");
 
IEnumerable<XElement> query =
    from e in xml.Descendants("Issue")
    where e.Ancestors("Message").Attributes("TypeName").First().Value == "ReviewSqlQueriesForSecurityVulnerabilities"
    select new XElement("Vulnerability",
        new XElement("Module", e.Ancestors("Module").Attributes("Name").First().Value),
        new XElement("Namespace", e.Ancestors("Namespace").Attributes("Name").First().Value),
        new XElement("Type", e.Ancestors("Type").Attributes("Name").First().Value),
        new XElement("Method", e.Ancestors("Member").Attributes("Name").First().Value),
        new XElement("Issue", e.Value)
        );
 
XDocument xDoc = new XDocument(
    new XDeclaration("1.0", "UTF-8", "yes"),
    new XElement("Vulnerabilities", query)
    );

Nota que con solo 2 instrucciones—la instrucción de carga no cuenta—logré hacer la transformación (¡!) smile_omg.   Veamos los cambios significativos al query.

Primero, el resultado es almacenado en una variable que ya no es tipo var, sino IEnumerable<T> donde T es XElement.  Esto es porque en la proyección (el select) ya no estoy utilizando un tipo anónimo para construir un objeto, sino los constructores de la clase XElement para obtener objetos de este tipo.

Finalmente, a la hora de crear el nuevo documento XML, utilizo también un constructor alterno de XDocument, para primero pasarle la declaración XML, y agregarle un elemento <Vulnerabilities> que tendrá como contenido el resultado de mi query—la lista de nodos <Vulnerability> con sus respectivos elementos hijo.  ¿Así o más mágico? smile_teeth

[Pronunciarse en tono de vendedor de chuchulucos por T.V.:] ¡Pero aún hay más!

¿Quieres saber lo que se necesita para guardar el nuevo documento de XML a un archivo?

xDoc.Save(@"C:\Users\Carlos\Desktop\transformedReport.xml");

Así que con un total de 4 instrucciones logré cargar un archivo complejo de XML, transformarlo y escribir los resultados a otro más amigable.

[Pronunciarse en tono de vendedor de chuchulucos por T.V.:] ¡Pero aún hay más!

Sintetizándo el código aún más

¿En verdad quieres ponerte quisiquilloso con el query?  Puedes lograr la totalidad de la lógica con solo 3 instrucciones—una para cargar el archivo original, otra para transformarlo y una más para guardar el archivo resultante—si utilizas expresiones lambda.  Este ejemplo no lo explico porque igual y te acabo confundiendo más, pero creo que ilustra el poderío de LINQ y las nuevas clases para manipular XML.

XElement xml = XElement.Load(@"C:\Users\Carlos\Desktop\sampleFxCopReport.xml");
 
XDocument xDoc = new XDocument(
  new XDeclaration("1.0", "UTF-8", "yes"),
    new XElement("Vulnerabilities",
      xml.Descendants("Issue")
      .Where(i => i.Ancestors("Message").Attributes("TypeName").First().Value == "ReviewSqlQueriesForSecurityVulnerabilities")
      .Select(e => new XElement("Vulnerability",
              new XElement("Module", e.Ancestors("Module").Attributes("Name").First().Value),
              new XElement("Namespace", e.Ancestors("Namespace").Attributes("Name").First().Value),
              new XElement("Type", e.Ancestors("Type").Attributes("Name").First().Value),
              new XElement("Method", e.Ancestors("Member").Attributes("Name").First().Value),
              new XElement("Issue", e.Value)
              )
          )
      )
  );
 
xDoc.Save(@"c:\users\Carlos\Desktop\transformedReport.xml");

¿Qué tal, eh? smile_nerd

Conclusión

Este artículo mostró un ejemplo de cómo transformar un documento XML utilizando LINQ to XML.  También mostró de manera práctica qué son los tipos anónimos (“clases sin nombres”), cómo cargar y escribir archivos XML con las clases XElement y XDocument incluidas en el .NET Framework 3.5, e ilustró brevemente el uso de expresiones lambda.  La mayoría de los conceptos—excepto quizá las expresiones lambda—aplican también si estás utilizando Visual Basic como tu lenguaje de desarrollo para .NET.

Enjoy. smile_shades

Google Chrome beta tampoco me impresiona

Todo mundo y su abuelita para ahora se ha de haber enterado que Google lanzó su propio browserWired tiene un artículo interesante de por qué se tardaron tanto para lanzarlo, cuando para muchas personas era una evolución lógica.

Bajo riesgo repetir artículos anteriores, tengo que admitir que cuando comencé a ver la lista de características “inovadoras” me sorprendió lo no inovadoras que en realidad son:

  • Omnibox. Básicamente una versión del Awesomebar (Firefox 3) o del Smart Address Bar de IE8 beta2, pero con búsqueda integrada. Moderadamente interesante. Se parece también al search box que trae Flock—y el cual espero fusionen con el Awesomebar para Flock 2 final.
  • Pestañas independientes. Rara vez se me ha ofrecido que una pestaña haga que truene el navegador. De las pocas veces que me ha sucedido es cuando corro IETab dentro de Firefox, e incluso en esas instancias FF hace buen trabajo en recuperar mi sesión. Pero, la idea es interesante. Tanto que al parecer se le ocurrió lo mismo al equipo de Internet Explorer 8.
  • Acceso directo a aplicaciones. Un concepto que se me hizo bastante útil, desde que el equipo de Mozilla comenzó con el proyecto Prism hace casi un año (¿?)—y del cual, naturalmente hay una extensión para Firefox.
  • Pestañas dinámicas. Yo suelo mantener una sola ventana de navegador abierta—si no ¿pa’ qué quiero las pestañas, verdad?—pero si les gusta esta característica, también pueden encontrarla en Safari.
  • Modo incógnito. O como dijo un amigo mío, modo “p0rn” jejeje—y no que yo lo use, ¿verdad? Hmm… ¿dónde he visto eso? ¡Ah! Qué tal en Safari (conocido como Private Browsing) y en IE 8 beta 2 (conocido como InPrivate).
  • Marcadores instantáneos. Ver Flock, y FF 3. No digo más.

De lo único que en verdad puedo ver inovador es el motor de JavaScript que le pusieron, V8, que al parecer patea traseros en desempeño. Y aunque algunos autores han argumentado que JavaScript es “solo una parte mínima del Web”, yo les digo: ¿Alguna vez has intentado correr Gmail, Slashdot o Facebook en IE7 o previos contra cualquier otro navegador moderno? JavaScript sí importa, y mucho.

Tratando de no tirar mucho rollo—demasiado tarde, lo sé—sí hubo cosas que me gustaron:

  • Desempeño. Jala hecho la mater cuando no está detrás de un proxy—lean un poco más abajo.
  • Google Gears integrado. No tienes que instalar esto por separado para utilizar, por ejemplo, Google Reader en modo offline. No sorprendente, dado que otro producto de Google, pero es útil que lo traiga integrado.
  • Interfaz minimalista. De las primeras cosas que le hago a Firefox es quitarle todos los botones y cosas extra y si vieran mi pantalla verían que mi arreglo quedó prácticamente idéntico al que usaron en Chrome. Safari también viene de cajón así.
  • Herramientas de desarrollo Web incluidas. Al igual que Safari, puedes seleccionar un elemento y dar “Inspect Element” para obtener información de CSS, etc.
  • Búsqueda en la página amigable. Casi igual que la de Safari—¿comienzan a ver un patrón?

image

Si quisiera resumir “lo bueno” diría que en muchos sentidos, Chrome es como Safari, pero sin el rendereo borroso de fonts en Windows y con Gears integrado.

Lo que de plano no me gusto, y de hecho impide que lo use de tiempo completo:

  • No le gustan los proxies, sobretodo los que te piden username y password. Ni siquiera puedes instalarlo si estás detrás de un proxy de este tipo. Y el desempeño se va por el caño, con muchas páginas que no se llegan a cargarse completas—no pude correr Gmail, lo cual es el colmo. Esto quizá es de esperarse de un beta—Safari tuvo problemas similares que resolvieron en la versión final—pero por ahora es un dealbreaker para mi.
  • Fallas de compatibilidad con sitios sociales. Algunas cosas en Facebook y MySpace no funcionan aún con Chrome. Unas simplemente hacen una experiencia menos agradable y otras de plano hacen que no puedas utilizar funcionalidad de estos sitios. MySpace puede irse a chiflar alpistle—es el sitio del demonio en lo que a mi concierne—pero Facebook está bastante bien armado hasta donde sé. Pero si el navegador no jala bien en estos sitios, ¿cuántos chavalos de 12 a 17 años que se la pasan viviendo ahí lo usarían?
  • No trae soporte para RSS. Solo muestra el XML del feed. ¿Cómo se supone que voy a leer mis noticias?

¿Matará Chrome a IE? Lo dudo. Habrá que ver lo que sucede. En mi opinión esto tomaría tiempo, aunque es interesante que algunos ya le achacan el 3% del mercado después de su lanzamiento. Google es una compañía a la par con los recursos y reputación de Microsoft, lo cual introduce una dinámica interesante, sobretodo para los que desarrollamos aplicaciones Web. Antes probablemente te preocupabas por que tus sitios jalaran en IE o Firefox, si mucho—al pobre de Opera nunca lo han pelado y mucho menos a Konqueror et.al. Ahora que tienes Safari y Chrome en el mercado, creo que finalmente comenzaremos una era donde los estándares en el desarrollo del Web cobren la importancia que debieron tener desde el principio. *Cruzando los dedos para que suceda*

Enjoy.