CSVGenerator

De BIS Wiki
Ir para navegação Ir para pesquisar

O CSVGenerator tem o objeto de permitir a geração de arquivos CSV a partir da estrutura de dados utilizada pelo BIS. Aproveito a estrutura dos VOs e BISMetaObjects (VO_), isto é, aceita uma lista de VOs, e gera o arquivo CSV a partir das colunas selecionadas de dentro desses objetos.

Para exemplificar o funcionamento de todas as partes do CSVGenerator vamos imaginar que queremos exportar como CSV nosso cadastro de pessoas, lista de PersonVO.

CSVGeneratorOptions

O CSVGeneratorOptions carrega praticamente toda a configuração de como o arquivo deve ser gerado. Desta forma é possível realizar toda a definição em qualquer cama (UI por exemplo) e passar para o gerador de uma única vez. Permite que seja criado um componente único para manipular o objeto para as UIs e reaproveitado em todas as telas, útil principalmente quando o gerador ganha novas opções.

Entre as configurações básicas do arquivo estão:

  • Delimitador - utilizado para separar os campos do arquivo
  • Localidade - Define a localidade para formatação básica dos valores, como caractere de decimais, data, etc.
  • Títulos das Colunas - Define se deve ser escrito uma linha no começo com os títulos das colunas.
  • Valor de Nulo - Valor que deve ser utilizado caso o objeto tenha um valor nulo. Por padrão não é escrito nada, o equivalente á "", mas pode ser necessário distinguir o "" do null.

CSVGenerator

Todos os atributos de configuração já tem um valor padrão, de forma que para gerar um CSV, basta definir as colunas desejadas e passar a coleção de dados no método .generate().


Java 256.png Exemplos de Uso
    // Define as colunas no options
    options.setColumns(new String[]{
        PersonVO_.VO.displayname(),
        PersonVO_.VO.fullname(), 
        PersonVO_.VO.cpfcnpj()
    });

    // Construtor vazio cria um CSVGeneratorOptions com a opções padrão. Mas depois tem que pegar o options para colocar as colunas ou terá um CSV vazio.
    final CSVGenerator<PersonVO> gen = new CSVGenerator<PersonVO>(options);

    // Retorna o CSV com as colunas selecionadas.
    return gen.generate(personList);


Note 64.png
Colunas Repetidas
Note que o nome da coluna é o identificador da coluna, por isso o CSVGenerator não aceita colunas com o mesmo nome, ou, no caso, colunas repetidas. Para inserir a mesma coluna duas vezes, (há vários casos para isso explicados mais a seguir) é necessário incluir uma coluna "personalizada". Também explicada mais a seguir.


O arquivo foi gerado, mas muitas vezes não é o que desejamos. Agora para formatar, configurar e manipular os dados é que precisará de configurações adicionais.


Formatando os Dados

A formatação dos dados é feita de forma simples e de acordo com o locale passado. São formatados os números e datas apenas. De resto os campos são escritos utilizando o equivalente ao .toString() do objeto, e apenas dos objetos conhecidos pelo CSVGenerator, como String, BigDecimal, Enum, Float, Double, etc.. Outros formatos geram exception crítica para forçar um tratamento mais adequado do desenvolvedor.

O CSVGenerator permite a configuração das colunas utilizando a interface BISDataFormatter. Para isso defina a instância do BISDataFormatter desejado na coluna através do CSVGeneratorOptions:

Java 256.png Exemplos de Uso
    // inclui o formatador de cpf/cnpj na coluna com o valor do documento
    options.getColumnFormatter().put(PersonVO_.VO.cpfcnpj(), BISCPFOrCNPJDataFormatter.getInstance());


Note 64.png
Formatação é o Último Passo
Note que a formação só é chamada no momento de escrever o objeto no arquivo CSV. Durante todo o processamento do arquivo e opções mais discutidas a seguir não recebem o dado formatado, mas sim no "Object" de acordo com o tipo de dado que foi recebido.


Processamento do Valor da Coluna

Em alguns casos desejamos realizar o processamento do valor da coluna, antes de passar pelo formatador, como por exemplo, caso o valor de uma determinada coluna seja truncado, ou seja realizado um arredondamento específico, etc.

Para estes casos devemos incluir um DataProcess, que nos fornecerá as informações necessárias para processarmos o valor conforme desejado.


Java 256.png Exemplos de Uso
    options.getColumnDataProcess().put(PersonVO_.VO.displayname(), new CSVColumnDataProcess() {
      @Override
      public Object getColumnValue(BISVO vo, String columnName, HashMap<String, Object> iterableCache) throws BISException {
        String t = (String) BISUtilsReflex.getPropertyValue(vo, columnName);
        // Trunca se tiver mais de 50 caracteres
        if (t != null && t.length() > 50) t = t.substring(0, 50);
        return t;
      }

      @Override
      public String getColumnTitle(String columnName) {
        // Retorna nulo para que seja recuperado normalmente pelo caption do BISAnnotation
        return null;
      }
    });


Note 64.png
ColumnDataProcess sobrepõe o funcionamento do CSVGenerator
Quando um ColumnDataProcess é definido para uma determinada coluna, o CSVGenerator não tenta busca o ID da coluna como se fosse um Path para pesquisar no VO. Ele passa direto para o ColumnDataProcess se virar com as informações.

Esse funcionamento é esperado, para evitar buscar valores que não existem, ou colunas personalizadas, como será explicado mais adiante.


Colunas Personalizada

Nem sempre a informação que desejamos escrever no CSV está dentro do VO. Como por exemplo, se queremos escrever uma coluna de "Total", sendo o resultado do cálculo envolvendo outros valores que estão no VO, mas este valor não está no VO diretamente. Ou qualquer outra coluna com qualquer outro valor, até mesmo escrever o System.currentTimeMillis() para verificar a velocidade de geração dos dados. Qualquer que seja a informação a ser escrita, você deve usar o mesmo ColumnDataProcess explicado acima e retornar qualquer valor desejado.

Como o ColumnDataProcess é colocado sempre 'por cima' de uma coluna existente, podemos realizar de duas maneiras: incluir uma coluna qualquer que não esteja sendo utilizada (só recomendável se o valor manipulado for de alguma maneira uma variação do valor original), ou criar uma coluna com um id customizado (mais recomendado), como por exemplo:


Java 256.png Exemplos de Uso
    // Define as colunas no options
    String columnTotal = "Total";
    options.setColumns(new String[]{
        ...,
        columnTotal,
        ..., 
        ...
    });

    ...

    options.getColumnDataProcess().put(columnTotal, new CSVColumnDataProcess() {
        ...
    });


Como as colunas com ColumnDataProcess não tentam resolver o valor no VO utilizando o ID da coluna, não resultará em erro.


Note 64.png
ID Personalizado sem ColumnDataProcess
Note que criar uma coluna com um ID qualquer (que não seja um path de atributos do VO) e não incluir o ColumnDataProcess fará justamente com que o CSVGenerator tente obter o valor através de reflexão no objeto, resultando em Exceptions!


Agrupando Valores

Em alguns momentos temos os dados todos detalhados, como estão na tabela, mas queremos apresentar os dados de forma agrupada, similar a funcionalidade do 'group by' utilizado no SQL. Para esses casos o CSVGenerator tem uma funcionalidade que permite que os registros sejam agrupados de acordo com campos chaves. Por exemplo, um relatório de vendas, temos todas as vendas de itens detalhada uma a uma, mas queremos exportar um arquivo com apenas 1 linha para cada código para cada empresa. Neste caso devemos definir a coluna de código como chave do agrupamento:


Java 256.png Exemplos de Uso
    options.setGroupByColumns(new String[]{
        companyColumnID,
        codeColumnID
    });


Desta forma, toda vez que o CSVGenerator encontrar um novo objeto com o mesmo valor na coluna companyColumnID e na codeColumnID, ela será "ignorada" já que já temos essa linha.

Funções de Agrupamento

O agrupamento de valores, conforme descrito acima, funciona bem se o objetivo for ter um funcionamento similar ao "Distinct" do SQL. Mas normalmente o agrupamento dos valores faz mais sentido quando desejamos "Sumarizar" os valores encontrados nas linhas repetidas. Se o caso for esse, devemos definir que função desejamos fazer com os outros valores um à um. Se for mesmo o caso de ir somando os valores a medida que as colunas chaves se repetem, devemos utilizar a função SUM. Definindo desta forma:


Java 256.png Exemplos de Uso
    // Faz com que os valores da coluna sejam todos somados a medida que as colunas chaves de repetem
    options.getGroupByFunctions().put(sumColumnID, CSVGroupByFunction.SUM);


Deste modo teremos apenas 1 linha com cada valor distinto das colunas definidas como chave, e os valores da coluna 'sumColumnID' serão somados.

O CSVGenerator também suporta outras funções como 'MIN' (Valor Mínimo), 'MAX (Valor Máximo), etc. Consulte o JavaDoc dos elementos da enum CSVGroupByFunction para mais detalhes sobre as funções.


Iterabilidade dos Objetos

A definição padrão dos dados para escrita em um CSV é uma tabela bi-dimensional. Sendo assim algumas estruturas de dados são difíceis (ou impossíveis) de se converter em uma tabela 2D. O CSVGenerator é capaz de iterar atributos que apresentem coleções automaticamente, inclusive em cascata. Desde que não tenhamos múltiplas "árvores" de iteração.


Imagine a seguinte estrutura de objetos:

voA
+- voB
+- voC
| +- listD<voD>
|   +- voE
|     +- listF<voF>
|       +- voG
+- voJ
  +- listH<voH>
    +- voI

Imagine que cada VO tenha seus atributos chamados at1, at2, etc.... Imagine que temos uma coleção de dados com 10 instâncias do voA, da qual desejamos criar nossos CSV, e que cada lista representada no diagrama acima também terá 10 instâncias dos seus objetos. Para essa mesma coleção de dados, teremos arquivos com quantidades diferentes de linhas dependendo das colunas que configurarmos. Por exemplo:

Caso 1: Configuramos as seguintes colunas:

"at1"
"voB.at1",
"voB.at2",
"voC.at1"

Neste caso teremos um arquivo final com 10 linhas, pois apenas a lista principal (coleção de voA) foi iterada e escrita uma linha para cada instância.


Caso 2: Configuramos as seguintes colunas:

"at1"
"voB.at1",
"voB.at2",
"voC.at1",
"voC.listD.at1",
"voC.listD.voE.at1"

Neste caso, as duas últimas linhas passam por um atributo que é uma coleção de dados. Ao se deparar com um conjunto de objetos o CSV passa a iterar esse novo objeto e repete todos os valores dos objetos que estão fora da iteração atual. Assim, para cada objeto principal (voA) iteramos a sua lista D. E como citamos acima, cada listaD também tem 10 instâncias, resultando em um arquivo de 100 linhas (10x10).

Caso 3: Tomando como base o caso 2, acrescentemos apenas a seguinte coluna adicional:

"voC.listD.voE.listF.voG.at1"

Note que agora que dentro de cada iteração da listD o CSVGenerator vai encontrar outra lista de voF, chamada de listF. Isso fará com que para cada voF encontrado dentro da listF todo o conteúdo das colunas anteriores seja repetido, modificando apenas o conteúdo dessa última coluna, que terá os valores do at1 do voG. Se o resultado do arquivo so caso 2 era de 100 linhas, e tivermos 10 objetos em cada listF encontrado, teremos agora um arquivo com 1000 linhas, sendo a cada 10 só teremos diferença na última coluna.


Caso 4: Se continuarmos o caso 2 ou caso 3, mas colocamos uma coluna que pertença a uma lista "fora" da cadeira que já estamos iterando, por exemplo:

"voJ.listH.at1"

Note que diferentemente da listF, que estava "dentro" da listD, a listH agora está "fora" das outras iterações. Desta forma não há como estabelecer uma relação entre os dados de cada instância dessa iteração com as iterações anteriores. Esse caso resultará em erro.


Note 64.png
Ideia de implementação do caso 4:
Durante a escrita desta documentação, uma ideia de conseguir gerar um arquivo com iterações independentes (só para evitar o erro), seria deixar nulo os valores dos campos que pertencem a iteração de "fora" da iteração atual. De forma que primeiro iteramos 1 caminho, seguindo as interações possível e deixando nulo os campos da outra iteração, e posteriormente iteramos a outra lista zerando os campos da iteração já realizada.

O resultado disso seria um arquivo com toda a informação desejada, com a repetição dos campos comuns (relacionáveis). Teríamos a quantidade de dados equivalente aos dois arquivos gerados com as colunas separadas, com a vantagem de ter uma única tabela para cruzamento com os dados já alinhados.