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 MonteiroCom 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.
Função |
Descrição |
string(obj?) |
Converte o parâmetro em string. |
concat(str, str, str*) |
Concatena todas as strings passadas como parâmetro em uma única string. |
starts-with(strValor, strInicio) |
Caso strValor começe com strInicio, retorna true. |
contains(strValor, strBusca) |
Caso strValor contenha strBusca, retorna true. |
substring(strValor, inicio, [total]) |
Retorna uma subcadeia da string, começando em [inicio] e com o tamanho [total]. Caso [total] não seja passado, retorna uma subcadeia de [inicio] até o fim da string. |
substring-before(strValor, strInicio) |
Retorna a subcadeia de strValor até o inicio de strInicio. |
substring-after(strValor, strInicio) |
Retorna a subcadeia de strValor após strInicio até o final da mesma. |
string-length(str?) |
Retorna o numero de caracteres do parâmetro. Caso não seja passado, retorna o tamanho do node corrente. |
normalize-space(str?) |
Normaliza os espaços em branco da string. Retira espaços em branco antes e depois do ultimo caracter significativo da string (algo como o Ltrim e Rtrim), e também troca os caracteres em branco entre as palavras da string por apenas um caracter. |
translate(strValor, strValoresBuscar, strValoresSubstituir) |
Retorna strValor, trocando cada ocorrência em strValoresBuscar pelo seu correspondente em posição em strValoresSubstituir. |
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?