Desenvolvimento - C#

Criando suas próprias extensões ao XPath em C#

O XPath é uma linguagem de acesso aos elementos de um documento Xml. Utilizando-a, pode-se acessar facilmente nodes ou grupos de nodes contidos em um documento, sem a necessidade de efetuar loops ou coisa parecida...

por Paulo Henrique dos Santos Monteiro



O XPath é uma linguagem de acesso aos elementos de um documento Xml. Utilizando-a, pode-se acessar facilmente nodes ou grupos de nodes contidos em um documento, sem a necessidade de efetuar loops ou coisa parecida.

Com o C#, é possível construir classes que extendem o XPath, possibilitando ao desenvolvedor acrescentar suas próprias funcionalidades a linguagem, obtendo resultados muito avançados em suas pesquisas.

Não é minha intenção aqui explicar a fundo o XPath, sendo este um assunto extenso, e que retornarei a abordar em artigos futuros, e sim, abordar como estender o XPath através do C#.

Para este artigo, e para ilustrar os exemplos, usarei um Xml de exemplo, que contém feriados brasileiros. Segue o Xml (para não ocupar muito espaço, tenho apenas os anos de 2004 a 2006). Os campos de data (Atributo DT_FERIADO no XML) estão em formato YYYYMMDD.

public const string FeriadosPadrao = @"<?xml version="1.0" encoding="ISO-8859-1"?>
<XML>
      <FERIADO DT_FERIADO = "20040101" DS_FERIADO="ANO NOVO"/>
      <FERIADO DT_FERIADO = "20040223" DS_FERIADO="CARNAVAL"/>
      <FERIADO DT_FERIADO = "20040224" DS_FERIADO="CARNAVAL"/>
      <FERIADO DT_FERIADO = "20040409" DS_FERIADO="SEMANA SANTA"/>
      <FERIADO DT_FERIADO = "20040421" DS_FERIADO="TIRADENTES"/>
      <FERIADO DT_FERIADO = "20040501" DS_FERIADO="DIA DO TRABALHO"/>
      <FERIADO DT_FERIADO = "20040610" DS_FERIADO="CORPUS CHRISTI"/>
      <FERIADO DT_FERIADO = "20040907" DS_FERIADO="INDEPENDÊNCIA DO BRASIL"/>
      <FERIADO DT_FERIADO = "20041012" DS_FERIADO="NOSSA SENHORA APARECIDA"/>
      <FERIADO DT_FERIADO = "20041102" DS_FERIADO="FINADOS"/>
      <FERIADO DT_FERIADO = "20041115" DS_FERIADO="PROCLAMAÇÃO DA REPÚBLICA"/>
      <FERIADO DT_FERIADO = "20041225" DS_FERIADO="NATAL"/>
      <FERIADO DT_FERIADO = "20050207" DS_FERIADO="CARNAVAL"/>
      <FERIADO DT_FERIADO = "20050208" DS_FERIADO="CARNAVAL"/>
      <FERIADO DT_FERIADO = "20050325" DS_FERIADO="SEMANA SANTA"/>
      <FERIADO DT_FERIADO = "20050421" DS_FERIADO="TIRADENTES"/>
      <FERIADO DT_FERIADO = "20050526" DS_FERIADO="CORPUS CHRISTI"/>
      <FERIADO DT_FERIADO = "20050907" DS_FERIADO="INDEPENDÊNCIA DO BRASIL"/>
      <FERIADO DT_FERIADO = "20051012" DS_FERIADO="NOSSA SENHORA APARECIDA"/>
      <FERIADO DT_FERIADO = "20051102" DS_FERIADO="FINADOS"/>
      <FERIADO DT_FERIADO = "20051115" DS_FERIADO="PROCLAMAÇÃO DA REPÚBLICA"/>
      <FERIADO DT_FERIADO = "20051225" DS_FERIADO="NATAL"/>
      <FERIADO DT_FERIADO = "20060227" DS_FERIADO="CARNAVAL"/>
      <FERIADO DT_FERIADO = "20060228" DS_FERIADO="CARNAVAL"/>
      <FERIADO DT_FERIADO = "20060414" DS_FERIADO="SEMANA SANTA"/>
      <FERIADO DT_FERIADO = "20060421" DS_FERIADO="TIRADENTES"/>
      <FERIADO DT_FERIADO = "20060501" DS_FERIADO="DIA DO TRABALHO"/>
      <FERIADO DT_FERIADO = "20060615" DS_FERIADO="CORPUS CHRISTI"/>
      <FERIADO DT_FERIADO = "20060907" DS_FERIADO="INDEPENDÊNCIA DO BRASIL"/>
      <FERIADO DT_FERIADO = "20061003" DS_FERIADO="ELEIÇÕES"/>
      <FERIADO DT_FERIADO = "20061012" DS_FERIADO="NOSSA SENHORA APARECIDA"/>
      <FERIADO DT_FERIADO = "20061102" DS_FERIADO="FINADOS"/>
      <FERIADO DT_FERIADO = "20061115" DS_FERIADO="PROCLAMAÇÃO DA REPÚBLICA"/>
      <FERIADO DT_FERIADO = "20061225" DS_FERIADO="NATAL"/>
</XML>";

Usando XPath para simplificar a obtenção de Nodes num documento XML

Digamos que eu sou um desenvolvedor que não conheça o XPath, e queira selecionar apenas os nodes dos feriados do ano de 2004.

Para isto, fatalmente faria um loop como o abaixo:

XmlDocument xml = new XmlDocument();
xml.LoadXml(FeriadosPadrao);
StringBuilder feriados2004 = new StringBuilder(300);
			
foreach(XmlNode node in xml.DocumentElement.ChildNodes)
{
if (node.Attributes["DT_FERIADO"].Value.CompareTo("20040101") >= 0 && 
   node.Attributes["DT_FERIADO"].Value.CompareTo("20041231") <= 0)
      feriados2004.AppendFormat("{0} em 
{1}\r\n",node.Attributes["DS_FERIADO"].Value,node.Attributes["DT_FERIADO"].Value);
}	
			
MessageBox.Show(feriados2004.ToString());

Ou seja, para selecionar um grupo de registros, teria-se que fazer um loop por todos os elementos do Xml. Isto é custoso, e um trabalho desnecessário.

Com XPath, temos a capacidade de filtrar os registros de um Xml quase como se estivéssemos utilizando um Sql. No caso, para filtrar os registros do ano de 2004 poderiamos fazer uma query XPath como:

string xPathSearch = "//FERIADO[@DT_FERIADO >= 20040101 and @DT_FERIADO <= 20041231]";

Uma pincelada de XPath. Traduzindo, seria algo como "Selecione todos os nodes FERIADO cujo atributo @DT_FERIADO (atributo é identificado com @ na frente do nome) for maior ou igual a 20040101 e menor ou igual a 20041231". Se prestar atenção, é muito parecido MESMO com o Sql.

No caso, nosso loop acima poderia ser simplificado como:

 //SelecNodes já devolve um NodeList apenas com os dados filtrados pela cláusula XPath
foreach(XmlNode node in xml.SelectNodes(xPathSearch))
feriados2004.AppendFormat("{0} em 
{1}\r\n",node.Attributes["DS_FERIADO"].Value,node.Attributes["DT_FERIADO"].Value);

Perceba que o nosso trabalho de comparação foi totalmente englobado pela cláusula XPath, sendo o nosso loop resumido apenas em concatenar os valores.

O XPath contempla diversas funções, como funções matemáticas, para tratamento de data/hora, etc. Para nosso exemplo, veja as funções para tratamento de String no XPath.

Perceba que realmente temos a mão ferramentas para tornar nossas pesquisas poderosas. Por exemplo, nossa cláusula XPath acima poderia ser simplificada como :

string xPathSearch	= "//FERIADO[starts-with(@DT_FERIADO,\"2004\")]";

Temos algumas outras funções, para tratamento de valores booleanos, números, etc.
A Microsoft também acrescentou algumas extensões ao XPath, através do seu namespace ms:.

Assim como a Microsoft, vamos agora abordar a maneira como podemos, via C#, estender o XPath, de modo a acrescentar as nossas próprias funções.

O PROBLEMA

Queremos, ainda usando o nosso Xml de exemplo, filtrar os feriados que caiam em dias de semana entre segunda e sexta-feira.

Se estivéssemos trabalhando com tipos DateTime, seria suficiente acessar o membro DayOfWeek, para sabermos qual o dia da semana.

DayOfWeek retorna uma enumeração, cujos valores correspondem de 0 (Domingo) a 6 (Sábado).

O nosso problema é que não é possível acessar diretamente, dentro de uma clausula XPath, o valor, ou engatar uma chamada a uma função C# diretamente. Então, para fazê-lo, devemos criar uma extensão do XPath, assim como fez a Microsoft, para acrescentar nesta extensão a nossa função customizada.

Iremos criar uma função chamada DayOfWeek, que internamente recebe uma string no formato YYYYMMDD, converte a mesma para DateTime, e retorna um inteiro representando o membro da enumeração System.DayOfWeek.

A SOLUÇÃO

Para criar funções customizadas, devemos criar um contexto para que as mesmas sejam implementadas. Um contexto pode conter uma miríade de funções, e é implementado criando uma classe derivada da classe abstrata XsltContext.

A classe XlstContext contempla 2 métodos: ResolveFunction() e ResolveVariable(). Ao localizar uma função na cláusula, é chamado o método ResolveFunction() do contexto customizado, que caso resolva a mesma Ok, retorna a função apropriada (que é uma classe derivada de IXsltContextFunction). A função, no caso, descreve o nome, tipos de argumentos, numero mínimo e máximo de parâmetros, e tipo de retorno.

Com isso, o analisador pode determinar se a chamada está correta, do ponto de vista da declaração da função. Em estando correta, pode fazer a chamada, utilizando para isto o método Invoke().

Uma nota especial diz respeito as chamadas variáveis de contexto. Variáveis de contexto são identificadas no XPath como $[nome da variável]. Estas variáveis são utilizadas como variáveis definidas pelo usuário, e podem receber valores de retorno, armazenar valores temporários em uma função, etc.

Ao usar uma variável de contexto, deveremos sobrescrever o método ResolveVariable() da classe XsltContext, e fornecer uma implementação de uma classe derivada de IXsltContextVariable, para poder retornar a referencia correta ao analisador. Isto é feito através da sobreposição do método Evaluate da classe, que nos retorna o valor correto da variável dentro da lista de argumentos.

Em primeiro lugar, vamos a implementação da nossa classe derivada de XsltContext.

Deveremos acrescentar os seguintes namespaces em nossa classe:

using System;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Globalization; // para uso especifico na DayOfWeek.

Nossa classe customizada (CustomXPath):

public class CustomXPath: XsltContext
{
private XsltArgumentList m_ArgList;

   public CustomXPath()
   {
   }
		
   public CustomXPath(NameTable nt) : base(nt)
   {
   }

   public CustomXPath(NameTable nt, XsltArgumentList argList) : base(nt)
   {
      m_ArgList = argList;
   }

   public XsltArgumentList ArgList
   {
      get
      { 
         return m_ArgList;
      }
   }

   public override IXsltContextFunction ResolveFunction(string prefix, 
         string name, XPathResultType[] ArgTypes)
   {
      CustomXPathFunction func = null;
   
      // interpreta o nome da função, retornando IXsltContextFunction
      switch (name)			
      {
	      case "DayOfWeek":
         // Uso 
         // DayOfWeek(string dataYYYYMMDD) retorna int
         // Traduzindo: Uma nova instancia de uma function p/ DayOfWeek, com 
         // numero mínimo e máximo de argumentos = 1, sendo que o parâmetro 
         // é uma string, retornando um inteiro
         func = new CustomXPathFunction("DayOfWeek", 1, 1, new 
         XPathResultType[] {XPathResultType.String}, XPathResultType.Number);
         break;
      }
      return func;
   }

   public override IXsltContextVariable ResolveVariable(string prefix, string name)
   {
         CustomXPathContextVariable Var;
         Var = new CustomXPathContextVariable(name);
         return Var;
   }

   // demais overrides omitidos para economia de espaço. Veja código completo .	
}

Ou seja, ao resolver o nome da function, retorna uma instancia da classe CustomXPathFunction, de modo que o analisador consiga determinar se, em primeiro lugar, a chamada está correta. Caso Ok, irá chamar o método Invoke da mesma.

Nossa implementação de IXsltContextFunction:

public class CustomXPathFunction: IXsltContextFunction
{
   private XPathResultType[] m_ArgTypes;
   private XPathResultType m_ReturnType;
   private string m_FunctionName;
   private int m_MinArgs;
   private int m_MaxArgs;

   public int Minargs
   {
      get
      {
         return m_MinArgs;
      }
   }

   public int Maxargs
   {
      get
      {
         return m_MaxArgs;
      }
   }

   public XPathResultType[] ArgTypes
   {
      get
      {
         return m_ArgTypes;
      }
   }

   public XPathResultType ReturnType
   {
      get
      {
         return m_ReturnType;
      }
   }

   public CustomXPathFunction(string name, int minArgs, int 
      maxArgs, XPathResultType[] argTypes, XPathResultType returnType)
   {
         m_FunctionName = name;
         m_MinArgs = minArgs;
         m_MaxArgs = maxArgs;
         m_ArgTypes = argTypes;
         m_ReturnType = returnType;
   }

   public object Invoke(XsltContext xsltContext, object[] args, XPathNavigator docContext)
   {
      object retorno = null;
			
      switch (m_FunctionName)
      {
         // FUNCIONALIDADE DA NOSSA DAYOFWEEK.
         case "DayOfWeek":
            DateTime dt = DateTime.ParseExact(args[0].ToString(),"yyyyMMdd",new CultureInfo("en-US"));
            retorno =  (int) dt.DayOfWeek;
            break;
      }
      return retorno;
   }
}

Não estamos usando neste exemplo, mas segue a implementação da IXsltContextVariable:

public class CustomXPathContextVariable:IXsltContextVariable
   {	
      private string m_VarName;

      public CustomXPathContextVariable(string VarName)
      {
         m_VarName = VarName;
      }

      public object Evaluate(XsltContext xsltContext)
      {
         XsltArgumentList vars = ((CustomXPath) xsltContext).ArgList;
         return vars.GetParam(m_VarName, null);
      }

      public bool IsLocal
      {
         get
         {
            return false;
         }
      }

      public bool IsParam
      {
         get
         {
            return false;
         }
      }

      public XPathResultType VariableType
      {
         get
         {
            return XPathResultType.Any;
         }
      }

   }

Para usar uma variável dentro de um namespace, devemos acrescentar a declaração da mesma, e acrescentar no construtor do contexto. Isto fará que a mesma seja utilizável em todas as funções que se precisar.

      XsltArgumentList varList = new XsltArgumentList();
      varList.AddParam("var", "", 2);
      CustomXPath contexto = new CustomXPath(new NameTable(),varList).

Se observarmos a implementação que fiz no método ResolveVariable, ele retorna uma referencia a CustomXPathContextVariable para o respectivo elemento da ArgList (alimentada por varList) do contexto ao qual pertence.

De acordo com o que expliquei anteriomente, o analisador irá invocar o contexto, para verificar se a função existe. Caso exista, o contexto devolve a função, que é verificada pelo analisador quanto aos parâmetros que estão sendo passados. Se estiver Ok, é chamado Invoke da classe, que por sua vez provém a inteligência pertinente ao método.

Usando a classe na cláusula XPath do exemplo

Vamos usar o nosso Xml de exemplo, mas agora acrescentando a chamada a nossa função customizada.

Em primeiro lugar, como ficaria a sintaxe.

string xPathSearch = "//FERIADO[starts-with(@DT_FERIADO,\"2004\") and 
nossasfunctions:DayOfWeek(string(@DT_FERIADO)) >= 1 and 
nossasfunctions:DayOfWeek(string(@DT_FERIADO)) < 6]";

Perceba que usei "nossasfunctions:". No XPath, isto é chamado namespace, usado para diferenciar o contexto no qual a function que segue os dois pontos está implementada. Isto serve, como no C#, para possibilitar que mais de um namespace implemente functions com mesmo nome.

Em seguida, é necessário instanciar o contexto.

CustomXPath	contexto	= new CustomXPath(new NameTable());

Ainda é necessário acrescentar um namespace para identificar este contexto.

contexto.AddNameSpace("nossasfunctions","http://nossasfunctions");

A url pedida é só para identificar o namespace. Não necessariamente precisa existir (como não existe mesmo, neste caso).

Para usar a cláusula com a nossa function, devemos passar o contexto como segundo parâmetro no overload para SelectNodes().

foreach(XmlNode node in xml.SelectNodes(XPathSearch,contexto))
{
feriados2004.AppendFormat("{0} em 
{1}\r\n",node.Attributes["DS_FERIADO"].Value,node.Attributes["DT_FERIADO"].Value);
}	

Ao debugar o código, tenha a curiosidade de colocar um breakpoint em ResolveFunction() e Invoke(), para ver a seqüência de execução das chamadas. É realmente muito interessante.

Como um exemplo de utilização da nossa function, vejamos um método para determinar o total de dias úteis entre duas datas.

private static int PrazoUtil(DateTime datainic, DateTime datafim)
{
   CultureInfo   culturaPadrao = new CultureInfo("en-US");
   XmlDocument   docFeriados = new XmlDocument ();
   string        xPathSearch = "//FERIADO[@DT_FERIADO >= " + 
datainic.ToString("yyyyMMdd",culturaPadrao) + " and @DT_FERIADO <= " + 
datafim.ToString("yyyyMMdd",culturaPadrao) + " and nossasfunctions:DayOfWeek(string(@DT_FERIADO)) 
>= 1 and nossasfunctions:DayOfWeek(string(@DT_FERIADO)) < 6]";
   CustomXPath   contexto = null;
   int           totalFer = 0;
   int           dayofweek = 0;
   int           totalDias = 0;
					
   try
   {
      contexto = new CustomXPath(new NameTable());
      docFeriados.LoadXml(FeriadosPadrao);
      contexto.AddNamespace("nossasfunctions","http://nossasfunctions ");
      XmlNodeList nodeList = docFeriados.SelectNodes(xPathSearch,contexto);
      totalFer = nodeList.Count;
				
      for(;;)
      {
         datainic = datainic.AddDays(1);
         dayofweek = (int) datainic.DayOfWeek;
         totalDias += ((dayofweek != 0 && dayofweek != 6) ? 1 : 0);
         if (datainic>datafim) break;
      }				
				
      totalDias-=totalFer;
				
   }
   catch(Exception e)
   {
      MessageBox.Show(e.Message.ToString());
   }
   return totalDias;
}

Ainda poderíamos implementar mais funcionalidades, como por exemplo, implementar uma função que, dadas duas datas, retornasse true se a data passada como parâmetro estivesse contida no período, etc. Mas, que tal se você agora a implementasse sozinho?

» Baixe o código.

Paulo Henrique dos Santos Monteiro

Paulo Henrique dos Santos Monteiro - Tecnólogo formado pela Faculdade de Tecnologia da Baixada Santista FATEC/BS, com 20 anos de experiência comprovada na área, tendo atuado em praticamente todas as áreas, desde saúde, epidemiologia, até automação bancária, software básico, comércio, indústria, backoffice bancário, jogos, segurança, prestação de serviços e consultoria. Também ministrei aulas de programação, análise e tópicos avançados de programação em escolas de 2º. Grau técnico e cursos particulares.
Iniciou com Basic, Clipper, Assembly e C, passando pelo C++ (em Unix, OS/2, Windows e DOS), e depois desenvolvendo sistemas com Delphi, Visual Basic (conheço desde a obscura versão DOS, e atuo com VB desde o Beta da versão 1), Prolog, Pascal, ASP, Javascript, Java, e com .NET e ASP.Net desde o Beta da primeira versão.
Atua também como DBA, e é grande entusiasta da programação armazenada, em Sybase, Sql Server e Oracle.
Publica dicas e códigos no seu blog,
http://taotecnologia.blogspot.com.