Acesso a sources via API

Objetivos

  • Compreender a forma de se ler os dados de um determinado source programaticamente nos diversos momentos onde isso pode ser necessário.
  • Entender, em cada momento, em que situação se encontra o source e os cuidados que devemos ter em sua manipulação.

Referências complementares

  • Controles
  • Process Actions
  • Sources
  • Data Providers
  • Post-load Processors
  • Ciclo de vida de uma interface DOUI
  • Clocks

O source dentro do ciclo de vida

O primeiro passo para entendermos as possibilidades de leitura e modificação de um source no Lumis é a correta compreensão dos elementos envolvidos na sua utilização e os momentos em que esse source pode ser acessado, seja para leitura ou modificação.

Um source nada mais é que um recipiente para dados, tipicamente (mas não obrigatoriamente) tabulares. O source possui as definições necessárias para que seja possível obter esses dados e popular o source, mas não é ele próprio quem faz isso. O objeto responsável por popular o source é o data provider.

Durante o ciclo de vida de uma interface, no momento em que os sources devem ser populados, o data provider é o responsável por obter os dados necessários, seja fazendo uma consulta ao banco, chamando um WebService ou ainda lendo um arquivo físico, e popular os sources com esses dados. O data provider é o objeto responsável por considerar filtros eventualmente configurados, além de parâmetros referentes à paginação, ordenação, etc.

Após o carregamento do source, existe uma etapa (opcional) de pós-processamento em que há possibilidade de modificar de alguma forma os dados existentes no source, seja adicionando ou removendo linhas (no caso de sources tabulares) ou até mesmo alterando os valores existentes. O objeto responsável por realizar esse pós-processamento é o Post-load Processor. O principal objetivo deste elemento é realizar alterações específicas no conteúdo de um source sem a necessidade de implementar um data provider customizado para isso.

Uma vez que o source esteja populado e, se for o caso, pós-processado, ele está pronto para ser utilizado pelos controles da interface, que farão acesso a seus dados para o que for necessário. Apenas os controles do tipo DataBoundControl têm ligação direta e nativa a um source, embora os demais controles também tenham a possibilidade de acesso, como veremos a seguir.

Por último, quando um process action é disparado para executar uma determinada ação ele também tem acesso ao source a ele associado. Entretanto, neste momento o source ainda não está carregado.

Veremos a seguir como é feito o acesso a um source em cada momento, bem como em um momento fora do ciclo de vida padrão, caso no qual há necessidade de criação do source a ser utilizado. É fundamental para uma boa arquitetura que o objetivo definido para cada um dos componentes citados anteriormente seja respeitado no que diz respeito ao contato com o source.

É muito importante destacar que, dependendo do momento, a criação de um source (ao invés da leitura de um source existente) e mesmo o comando para carga de um source existente pode gerar efeitos colaterais indesejáveis, sendo portanto necessário que alguns cuidados sejam tomados. A seguir estes cenários serão abordados em maiores detalhes.

O acesso ao source em cada momento

DataProviders (O carregamento do source)

Uma vez que o data provider é o elemento responsável por carregar um source com os seus dados, no início do seu processamento o source se encontra, obviamente, vazio. Sendo assim, o único acesso que faz sentido durante essa etapa é o próprio carregamento do source.

Todo data provider deve possuir um método chamado loadData, em que os dados serão inseridos no source passado como parâmetro para esse método. O trecho de código a seguir ilustra uma implementação da função loadData que popula um source com os dados lidos de uma matriz de inteiros.


public void loadData(SessionConfig sessionConfig, TabularSource<?> source, ITransaction transaction) throws PortalException
{
       // Matriz de valores inteiros que será usada para popular o source
       int[][] matriz = {{1,2,3},{4,5,6},{7,8,9}};
       
       // Obtemos o objeto tabularData, que é onde são armazenados os dados
       TabularData tabularData = source.getData();
       
       for (int[] linha : matriz)
       {
             // Para cada linha na matriz criamos uma linha no tabularData
             ISourceData newRow = tabularData.addRow();
             int numColuna = 1;
             
             for (int coluna : linha)
             {
                    // Para cada coluna cria um campo com nome campo1, campo2, ...
                    newRow.put("campo"+numColuna, coluna);
                    numColuna++;
             }
       }
       
       // Ao final o source está populado com 3 linhas, cada uma delas com 3 colunas
}

Post-Load Processors (O pós-processamento de um source)

Nesse momento o source já se encontra populado, e portanto o objeto TabularData já possui dados.

O post-load processor pode manipular esses dados adicionando ou removendo linhas e/ou modificando o valor de colunas em um método chamado processSource. A seguir é mostrado um exemplo onde o valor de um determinado campo faz com que haja remoção de linhas ou alteração do próprio valor.


publicvoid processSource(SessionConfig sessionConfig, Source source,
             Node parametersNode, ITransaction transaction)
             throws PortalException 
{
       // Obtém o objeto Tabular Data, que guarda os dados do source
       TabularData tabularData = (TabularData) source.getData();
       
       // Obtém as linhas existentes no source para iteração
       List<ISourceData> rows = tabularData.getRows();
       
       for (ISourceData row : rows)
       {
             // Para cada linha obtém o valor do campo "tipo"
             String tipo = row.get("tipo", String.class);
 
             if(tipo.equals("excluir"))
             {
                    // Se o valor do campo tipo é "excluir", remove a linha
                    rows.remove(row);
             }
             elseif(tipo.equals("modificar"))
             {
                    // Se for "modificar", altera o valor do campo para "modificado"
                    row.put("tipo", "modificado");
             }
       }             
}

Controles (O acesso aos dados de um source)

Os controles, como objetos responsáveis pela renderização, fazem acesso aos dados existentes no source e os utiliza para gerar uma estrutura XML.

No caso de DataBoundControls, como esses objetos são nativamente associados a um source (através do atributo sourceId presente na definição do controle) o acesso é direto. A seguir, um trecho de código do método setRenderData de um controle onde os dados de um source são lidos e organizados em um XML que será utilizado para renderização do controle.


// Objeto para concatenar a string com o xml de resultado
StringBuilder results = new StringBuilder();
 
// Obtém o source associado a si através do método getSource
TabularSource<?> source = getSource();
 
// Obtém o objeto tabular data, que contém os dados
TabularData tabularData = source.getData();
 
// Se não existem dados no source escreve apenas uma mensagem
if(tabularData.isEmpty())
{
       results.append("<mensagem>Source vazio</mensagem>");
}
 
// Inicia a string de resultados
results.append("<data>");
 
for(ISourceData row : tabularData.getRows())
{
       // Para cada linha existente no source gera uma row no XML
       results.append("<row>");
       
       // Lê o valor do campo "campo1" e escreve no xml
       results.append("<campo1>");
       results.append(row.get("campo1"));
       results.append("</campo1>");
       
       // Lê o valor do campo "campo2" e escreve no xml
       results.append("<campo2>");
       results.append(row.get("campo2"));
       results.append("</campo2>");
       
       results.append("</row>");
}
 
// Finaliza a string de resultados
results.append("</data>");

Repare que no exemplo acima foi utilizado o método getSource, que já retorna o objeto source associado ao controle, porém é possível obter da mesma forma um outro source da interface que não seja o associado a esse controle. Para isso, ao invés de usar o método getSource basta usar getSourceById, passando o identificador do source que se deseja obter. A partir daí, a forma de interação com esse source é a mesma.

No caso de controles que não sejam DataBoundControls, ou seja, Controls ou DataControls, o acesso ao source é possível embora não seja tão direto, visto que esses outros tipos de controle, conceitualmente, não são ligados a sources. Nesse caso é necessário saber o id do source que se deseja obter, já que nenhum está associado ao controle. O trecho de código a seguir mostra como se obter um source dessa forma através do objeto douiContext, nativo de todos os controles.


Source source = douiContext.getSourceContainer().getSourceById(sourceId);

Repare que, se um controle customizado precisar acessar um source por alguma razão, provavelmente ele deve ser do tipo DataBoundControl. Sendo assim, embora seja tecnicamente possível obter dados de um source a partir de um controle que não seja deste tipo, tal necessidade pode indicar um problema na arquitetura do controle.

Process actions (Acesso a informações do source)

Um process action também é associado a um source, já que ele pode precisar, por exemplo, inserir dados que causarão a modificação dos dados retornados por esse source. Por este motivo o objeto ProcessAction já possui um ponteiro para o objeto que representa o source a ele associado como variável da classe, de modo que o acesso para obter quaisquer definições do source pode ser feito diretamente sobre a variável source.

No entanto, durante um process action os sources não estão populados, de modo que o objeto existe essencialmente para que seja possível a leitura de configurações. Isso pode ser utilizado, por exemplo, no caso de um source que representa uma tabela no banco de dados, para que seja possível a obtenção do nome da tabela, para realizar uma inserção.

Não é aconselhável que seja forçado o carregamento de um source (através do método load) sobre o contexto existente para um ciclo de vida. Isso porque a ação de carregamento de um source pode disparar outras ações que influenciam o ciclo de vida como um todo. Assim, forçar o carregamento em um momento onde normalmente o source não seria carregado pode trazer efeitos colaterais indesejáveis para o fluxo de renderização da interface.

Caso seja necessário obter os dados de um determinado source durante um process action por alguma razão, o ideal é que esse source seja criado sobre um novo contexto, evitando a modificação do contexto existente e por consequência todo o ciclo de vida da interface. Para isso é necessário que esse novo contexto seja também criado.

A seguir veremos como é possível criar um source e obter seus dados de forma independente do contexto em questão, ou em um cenário onde não existe um contexto prévio.

Criando e populando um source em um novo contexto

Nos tópicos anteriores verificamos, para cada momento do ciclo de vida, como acessar os sources, qual deve ser o principal objetivo e a situação em que eles se encontram em cada momento. O quadro abaixo resume esses cenários.

Momento do ciclo de vida Estado do source Motivo do acesso ao source
No data provider Não populado Popular o source
No post-load processor Populado Realizar alterações nos dados existentes no source
No controle Populado Obter dados para renderização
No process action Não populado Obter configurações do source

Em todos os momentos citados, estamos acessando um source existente dentro do contexto de um ciclo de vida. Qualquer manipulação não prevista, como forçar o carregamento do source em qualquer um desses momentos, pode gerar resultados imprevistos sendo, portanto, desaconselhada.

Veremos a seguir uma forma de criar um source sobre um novo contexto, totalmente desassociado do contexto existente durante o ciclo de vida de uma interface. Tecnicamente, essa forma pode ser usada tanto em um cenário em que não existe um contexto, como durante o ciclo de vida. Como usualmente a utilização desta forma de acesso a um source durante o ciclo de vida indica falhas na arquitetura do serviço, é recomendável que seja feita uma análise cuidadosa para avaliar a real necessidade de se utilizar essa abordagem.

Vamos considerar um exemplo de cenário em que um processo agendado para executar periodicamente precise verificar o número de notícias publicadas em uma determinada seção do site através de um serviço do tipo Content, e notificar o usuário publicador caso não haja notícias publicadas.

Não é possível simplesmente fazer um acesso ao banco de dados para realizar a contabilização, uma vez que para a notícia ser publicada ela precisará estar dentro dos critérios de publicação definidos. Implementar a verificação desses critérios no processo, por outro lado, seria excessivamente custoso.

Uma boa opção nesse caso, com um processo agendado sendo executado em uma thread paralela e totalmente fora de qualquer contexto DOUI, é criar um contexto e, sobre esse, criar o source com base nas suas definições forçando o seu carregamento para que possamos contabilizar o número de notícias efetivamente publicadas. O trecho a seguir obtém a definição de um source a partir do ‘id’ do serviço, cria o contexto sobre uma determinada instância de serviço (que assumimos já possuir o id a priori), além de criar e forçar o carregamento do source sobre esse contexto para realizar a validação.


// Obtemos o XML de definição DOUI do serviço em questão
Node douiServiceDefinitionNode = DouiManagerFactory.getDouiManager().getDouiServiceDefinitionNode("exemplo.service.noticias", transaction);
 
// Obtemos o XML de definição do source em questão
Node sourceDefinitionNode = XmlUtil.selectSingleNode("service/sources/source[@id='noticias']", douiServiceDefinitionNode);
 
// Criamos o contexto para uma determinada instância de serviço
ServiceInstanceConfig serviceInstanceConfig = ManagerFactory.getServiceInstanceManager().get(serviceInstanceId, transaction);
ISourceContext sourceContext = new SourceContext(sessionConfig, serviceInstanceConfig, transaction);
 
// Criamos o source, usando sua definição e o contexto criado
TabularSource source = (TabularSource) SourceFactory.createSource(sourceDefinitionNode, sourceContext);
 
// Forçamos o carregamento do source
source.setLoad(true);
source.load();
 
// Obtemos o objeto que contém os dados desse source
TabularData tabularData = source.getData();
 
// Se o source estiver vazio, notifica o publicador
if(tabularData.isEmpty())
       notificaPublicador();

Repare que neste caso não são aplicados sobre o source parâmetros que podem afetar o seu carregamento, como por exemplo a definição do número de itens por página, da página em questão e a aplicação de parâmetros para filtragem dos resultados. Para isso precisaríamos atribuir esses valores através de métodos específicos (setMaxRows, setStartAt e, para passar parâmetros para o carregamento do source o método setParameterValue) antes de invocar o método load.

Neste exemplo criamos um source em um novo contexto em uma classe do tipo clock, mas poderíamos aplicar essa mesma abordagem para criar um source em uma classe chamada a partir de um .jsp, em uma classe de uma interface genérica ou em qualquer outra situação fora do ciclo de vida DOUI.