domingo, 5 de agosto de 2018

JPQL - Parte 1/3

Introdução

A especificação JPA existe para facilitar o mapeamento de objeto com o banco de dados relacional. Apesar da especifição conter a JPQL e a Criteria, ambas precisam de uma curva de aprendizado grande, sendo que a Criteria acaba sendo muito verboso para uma simples busca de dados. A biblioteca de JPQL do JARCH resolve esse problema. Essa biblioteca é muito poderosa sendo possível fazer buscas de JPQL de forma fluente. Nessa primeira parte do post vamos fazer alguns exemplos de busca de dados com filtros.

Exemplos

Nesse primeiro exemplo vou fazer uma simples busca de todos os lançamentos que contenha na descrição a palavra Aluguel.
Segue o exemplo abaixo:
Collection<LancamentoEntity> listaAluguel = LancamentoJpaqlBuilder
.newInstance()
.where()
.contains("descricao", "Aluguel")
.collect()
.list();
Agora vou fazer uma busca do lançamento com o código 000564:
LancamentoEntity lancamento = LancamentoJpaqlBuilder
.newInstance()
.where()
.equalsTo("codigo", "000564")
.collect()
.single();
Nos dois exemplos anteriores utilizamos o nome do atributo como uma String, o problema disso é que se estivesse digitado errado o nome do atributo ocorreria um erro somente em tempo de execução. Para evitar esse problema, vamos usar o atributo tipado, conforme o exemplo abaixo:
LancamentoEntity lancamento = LancamentoJpaqlBuilder
.newInstance()
.where()
.equalsTo(LancamentoEntity_.codigo, "000564")
.collect()
.single();
Pode ser que não exista o código 000564, então vou usar uma programação defensiva para não tomar uma exceção caso o código não exista. Vou também adicionar um import estático para LancamentoEntity_.codigo, segue o exemplo abaixo:
Optional<LancamentoEntity> lancamento = LancamentoJpaqlBuilder
.newInstance()
.where()
.equalsTo(LancamentoEntity_.codigo, "000564")
.collect()
.singleOptional();

if (lancamento.isPresent()) {
// Executa a logica...
}

Vou fazer agora uma busca um pouco mais elaborada, vou pesquisar os lançamentos de 2016 e 2018 e que o valor seja maior ou igual que R$ 1.000,00 (Hum Mil).
Segue o exemplo abaixo:
Date dataInicio2016 = DateUtils.toDate(LocalDate.of(2016, Month.JANUARY, 1));
Date dataFim2016 = DateUtils.toDate(LocalDate.of(2016, Month.DECEMBER, 31));

Date dataInicio2018 = DateUtils.toDate(LocalDate.of(2018, Month.JANUARY, 1));
Date dataFim2018 = DateUtils.toDate(LocalDate.of(2018, Month.DECEMBER, 31));

Collection<LancamentoEntity> lancamento = LancamentoJpaqlBuilder
.newInstance()
.where()
.openParenthesis()
.openParenthesis()
.greaterOrEqualsThan(LancamentoEntity_.vencimento, dataInicio2016)
.and()
.lessOrEqualsThan(LancamentoEntity_.vencimento, dataFim2016)
.closeParenthesis()
.or()
.openParenthesis()
.greaterOrEqualsThan(LancamentoEntity_.vencimento, dataInicio2018)
.and()
.lessOrEqualsThan(LancamentoEntity_.vencimento, dataFim2018)
.closeParenthesis()
.closeParenthesis()
.and()
.greaterOrEqualsThan(LancamentoEntity_.valor, BigDecimal.valueOf(1000))
.collect()
.list();
Agora vou selecionar todos os lançamentos que não houve nenhum pagamento:
Collection<LancamentoEntity> lancamentos = LancamentoJpaqlBuilder
.newInstance()
.where()
.collectionEmpty(LancamentoEntity_.listaLancamentoPagamento)
.collect()
.list();
Outra condição poderia avaliar se existe centro de custo no lançamento:
Collection<LancamentoEntity> lancamentos = LancamentoJpaqlBuilder
.newInstance()
.where()
.exists(LancamentoEntity_.centroCusto)
.collect()
.list();
Conclusão

Nesse primeiro post sobre a JPQL do JARCH fizemos alguns exemplos de filtros, estes filtros seriam um pouco complicado e verboso sem o uso do JARCH. Essa biblioteca facilita o uso do JPQL, trazendo facilidade para buscas de entidades. No próximo post veremos outros exemplos.

Até mais,

sexta-feira, 3 de agosto de 2018

Conversores JPA

Introdução

A JPA possui um recurso para criar conversores JPA, sendo um facilitador para converter qualquer tipo de objeto, ENUM. O JARCH possui alguns conversores que facilita a gravação de alguns tipos.

Exemplos

Nesse post vou mostrar como usar os conversores JPA. O JARCH possui alguns conversores JPA para a nova API de datas, boolean, etc. Existe duas formas de utilizar esses conversores, colocando o conversor através da anotação @Convert ou registrando os conversores no persistence.xml.
Segue um exemplo abaixo usando a declaração via anotação:
1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
package br.com.arch.arquitetura.crud.tudo;

import br.com.jarch.crud.entity.CrudMultiTenantEntity;
import br.com.jarch.jpa.converter.LocalDateJpaConverter;
import br.com.jarc.jpa.converter.LocalDateTimeJpaConverter;
import br.com.jarch.jpa.converter.LocalTimeJpaConverter;
import br.com.jarch.jpa.converter.YearMonthJpaConverter;
import br.com.jarch.jpa.converter.br.BooleanSNJpaConverter;
import org.hibernate.envers.Audited;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;

@Audited
@Table(name = "tb_tudo")
@Entity(name = "tudo")
@SequenceGenerator(name = "TudoIdSequence", sequenceName = "sq_idtudo", allocationSize = 1)
public class TudoEntity extends CrudMultiTenantEntity {

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO, generator = "TudoIdSequence")
 @Column(name = "id_tudo")
 private Long id;

 @Column(name = "dt_tudo")
 @Convert(converter = LocalDateJpaConverter.class)
 private LocalDate data;

 @Column(name = "dh_tudo")
 @Convert(converter = LocalDateTimeJpaConverter.class)
 private LocalDateTime dataHora;

 @Column(name = "hr_tudo")
 @Convert(converter = LocalTimeJpaConverter.class)
 private LocalTime hora;

 @Column(name = "am_competencia", length = 7)
 @Convert(converter = YearMonthJpaConverter.class)
 private YearMonth competencia;

 @Column(name = "sn_tudo")
 @Convert(converter = BooleanSNJpaConverter.class)
 private Boolean simNao;

 @Override
 public Long getId() {
  return id;
 }

 @Override
 public void setId(Long id) {
  this.id = id;
 }

 public LocalDate getData() {
  return data;
 }

 public void setData(LocalDate data) {
  this.data = data;
 }

 public LocalDateTime getDataHora() {
  return dataHora;
 }

 public void setDataHora(LocalDateTime dataHora) {
  this.dataHora = dataHora;
 }

 public LocalTime getHora() {
  return hora;
 }

 public void setHora(LocalTime hora) {
  this.hora = hora;
 }

 public YearMonth getCompetencia() {
  return competencia;
 }

 public void setCompetencia(YearMonth competencia) {
  this.competencia = competencia;
 }

 public Boolean getSimNao() {
  return simNao;
 }

 public void setSimNao(Boolean simNao) {
  this.simNao = simNao;
 }
}

Analisando o código anterior notamos a utilização da anotação @Convert usando o conversor do JARCH.
A outra maneira de utilização seria registrar os conversores no persistence.xml. Dessa maneira todos os campos de uma Entity que obtiverem os conversores registrados já terá o conversor implicitamente.
Segue um exemplo abaixo declarando os conversores no persistence.xml:

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="arquiteturaweb-pu" transaction-type="JTA">
        <jta-data-source>java:/ds/arquitetura/postgresql</jta-data-source>
        <class>com.arch.jpa.converter.LocalDateJpaConverter</class>
        <class>com.arch.jpa.converter.LocalDateTimeJpaConverter</class>
        <class>com.arch.jpa.converter.LocalTimeJpaConverter</class>
        <class>com.arch.jpa.converter.YearMonthJpaConverter</class>
        <class>com.arch.jpa.converter.br.BooleanSNJpaConverter</class>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>

        <!-- <exclude-unlisted-classes>false</exclude-unlisted-classes> -->
        <properties>

No código anterior as linhas com as tags declara os conversores, mas ainda existe uma outra forma de declarar todos os conversores sem ficar especificando um a um.
Segue o persistence.xml abaixo com a declaração de todos os conversores sem especificar um a um:

1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="arquiteturaweb-pu" transaction-type="JTA">
        <jta-data-source>java:/ds/arquitetura/postgresql</jta-data-source>

        <jar-file>lib/jarch-framework-18.7.1.jar</jar-file>

        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>

        <!-- <exclude-unlisted-classes>false</exclude-unlisted-classes> -->
        <properties>

Conclusão

Com esses conversores disponíveis o uso da nova API de datas ficou mais fácil, tornando transparente a migração do antigo Date.

Até mais,


quinta-feira, 2 de agosto de 2018

Configuração Menu

Introdução

A criação de menus sempre foi um pouco complicado mesmo utilizando framework's como JSF e Primefaces ou até mesmo utilizando HTML5, mas seria muito melhor se houvesse uma API JAVA para isso. O JARCH possui uma API para criação de menus de forma fluente, tornando muito fácil sua utilização.

Exemplos

Nesse post vou mostrar como configurar o menu da aplicação usando JARCH.
Segue abaixo um print do menu do template JARCH:
Para a implementação do menu é necessário criar uma action estendendo do MenuBaseAction.
Segue abaixo um exemplo:
1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package br.com.arch.arquitetura.menu;

import br.com.jarch.annotation.ArchViewScoped;
import br.com.jarch.menu.BaseMenuAction;
import br.com.jarch.menu.IMenu;
import br.com.jarch.menu.MenuBuilder;

import java.util.ArrayList;
import java.util.List;

@ArchViewScoped
public class MenuAction extends BaseMenuAction {

    @Override
    public List<IMenu> createMenu() {
        List<IMenu> menu = new ArrayList<>();

        menu.add(MenuBuilder
                .newInstance()
                .id("liCadastro")
                .name("Cadastro")
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .id("liCadastroAplicacao")
                        .name("Aplicação")
                        .action("../aplicacao/aplicacaoList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .id("liCadastroBanco")
                        .name("Banco")
                        .action("../banco/bancoList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Condominio")
                        .action("../condominio/condominioList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Cliente")
                        .action("../cliente/clienteList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Marca")
                        .action("../marca/marcaList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Produto")
                        .action("../produto/produtoList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Pessoa")
                        .action("../pessoa/pessoaList.jsf")
                        .build())
                .build());

        menu.add(MenuBuilder
                .newInstance()
                .name("Lançamento")
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Contas Pagar")
                        .action("../contapagar/contaPagarList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Pedido")
                        .action("../pedido/pedidoList.jsf")
                        .build())
                .build());

        menu.add(MenuBuilder
                .newInstance()
                .name("BPM")
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Lista Tarefa")
                        .action("../bpmn/tarefaList.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Lista Cockpit")
                        .action("../bpmn/listCockpit.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Lista Incidente")
                        .action("../bpmn/listIncident.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Reprocessamento")
                        .action("../bpmn/reprocessamento.jsf")
                        .build())
                .addSubMenu(MenuBuilder
                        .newInstance()
                        .name("Deploy")
                        .action("../bpmn/deploy.jsf")
                        .build())
                .build());

        menu.add(MenuBuilder
                .newInstance()
                .name("Parâmetro")
                .action("../relatorio/relatorioProduto.jsf")
                .build());

        return menu;
    }
}

Acima estou criando uma classe com o nome de MenuAction com escopo de view e acessível via EL (@ArchViewScoped). A classe que está sendo estendida de MenuBaseAction, essa classe obriga a implementação do método createMenu, que é uma List<IMenu>. Essa List<IMenu> será os itens que aparecerão no menu, para preencher essa lista eu usei a biblioteca MenuBuilder, e de forma fluente vou preenchendo o menu principal com os itens de menus.

Conclusão

Nunca foi tão fácil a criação de menu e mesmo se o menu for de forma dinâmica a utilização dessa biblioteca é muito fácil.

Até mais,

Implementação Login

Introdução

Criação de login nem sempre é uma tarefa fácil, mas o JARCH já possui o template e com uma pequena implementação de uma classe com um método toda essa estrutura estará pronta.

O JARCH já possui alguns templates, dentre eles o login. Para implementação do login na aplicação basta somente criar uma action que estenda a classe base de login.
O print abaixo da página de login (template do JARCH).
Essa página possui a seguinte action:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package br.com.arch.arquitetura.login;

import br.com.jarch.arquitetura.usuario.UsuarioFachada;
import br.com.jarch.annotation.ArchViewScoped;
import br.com.jarch.crud.action.BaseLoginAction;
import br.com.jarch.crud.action.IBaseLoginAction;
import br.com.jarch.user.IUser;

import javax.inject.Inject;
import java.io.Serializable;

@JArchViewScoped
public class LoginAction extends BaseLoginAction implements IBaseLoginAction, Serializable {

    @Inject
    private UsuarioFachada usuarioFachada;

    @Override
    public IUser processLogin() {
        return usuarioFachada.login(getLogin(), getPassword());
    }

    @Override
    public void forgotPassword(String loginForgotPassword) {

    }
}
Vamos analisar o código acima
Primeiro na linha 12 usei a anotação que é um esteriótipo CDI do framework para informar que essa instância terá o escopo de view e será acessível via EL.
A linha 13 é a declaração da classe LoginAction que está estendendo a classe BaseLoginAction e por ser abstrata obriga a implementação de 2 métodos, processLogin() e forgotPassword().
O processLogin() é a validação do login e senha informado na página de login.
O forgotPassword() é usado quando o usuário informar que esqueceu a senha.

Conclusão

Com essa estrutura pronta do JARCH a implementação de regra do login é muito simples.

Até mais,

quarta-feira, 1 de agosto de 2018

JARCH Framwork MVC

Introdução

Neste Post vou comentar sobre o que é o JARCH Framework MVC. Esse framework foi desenvolvido para diminuir a curva de aprendizado no desenvolvimento de aplicações corporativas utilizando a plataforma JAVAEE.

Resumo

De forma bem resumida o JARCH traz alta produtividade para um desenvolvimento JAVAEE rápido e fácil, utilizando com base suas especificações. Composto por diversos frameworks, componentes e bibliotecas para aplicações corporativas! Possui nativamente internacionalização I18N, auditoria de dados, dados sigilosos e multi-tenant. Diversas bibliotecas para geração de testes de sistema, geração de relatórios, consulta JPQL e muito mais. Encapsulamos o BPMN, utilizando o Camunda como motor e o Modeler para o design. Temos uma ferramenta GCA para geração dos artefatos. Possui também diversos APT's (Annotations Processors) que auxiliam na geração de artefatos e garante a padronização na compilação. Para a camada de visão temos diversos componentes personalizados (Composites Components).

A geração dos artefatos no JARCH é através de APT (Annotation Processor Tools), onde basicamente se configura as anotações dentro do package-info.java e no momento da compilação os artefatos são gerados. Essas anotações podem ser criadas em qualquer IDE ou editor, mas existe uma ferramenta chamada CGA (Generate Code Annotation) que facilita a geração dessas anotações. Abaixo segue um exemplo do uso dessas anotações.


1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@JArchLogicMasterSubDetail(nameSubPackage = "marca",
  master = @JArchMaster(name = "Marca", tableName = "tb_marca",
    fields = {
      @JArchField(fieldName = "codigo", fieldTable = "cd_marca", description = "Código",
                  type = FieldType.CODIGO, required = true, showDataTable = true, showSearch = true),
      @JArchField(fieldName = "nome", fieldTable = "nm_marca", description = "Nome", 
                  type = FieldType.NOME, required = true, showDataTable = true, showSearch = true, 
                  columnSearch = 2, rowDataXhtml = 2)
    }),
  details = {
    @JArchDetailSubDetail(name = "Modelo", tableName = "tb_marca_modelo",
      fields = {
              @JArchField(fieldName = "codigo", fieldTable = "cd_modelo", description = "Código", 
                          type = FieldType.CODIGO, required = true, showDataTable = true, showSearch = true),
              @JArchField(fieldName = "nome", fieldTable = "nm_modelo", description = "Nome", 
                          type = FieldType.NOME, required = true, showDataTable = true, showSearch = true, 
                          columnSearch = 2, rowDataXhtml = 2)
      },
      subDetails = {
              @JArchSubDetail(name = "SubModelo", tableName = "tb_marca_modelo_submodelo",
                  fields = {
                    @JArchField(fieldName = "codigo", fieldTable = "cd_submodelo", description = "Código", 
                                type = FieldType.CODIGO, required = true, showDataTable = true, 
                                showSearch = true),
                    @JArchField(fieldName = "nome", fieldTable = "nm_submodelo", description = "Nome", 
                                type = FieldType.NOME, required = true, showDataTable = true, 
                                showSearch = true, columnSearch = 2, rowDataXhtml = 2)
                  }
              )
      }
    )
  }
)

package br.com.arch.arquitetura.crud;

import com.arch.annotation.JArchDetail;
import com.arch.annotation.JArchDetailSubDetail;
import com.arch.annotation.JArchField;
import com.arch.annotation.JArchLogicCrud;
import com.arch.annotation.JArchLogicMasterDetail;
import com.arch.annotation.JArchLogicMasterSubDetail;
import com.arch.annotation.JArchMaster;
import com.arch.annotation.JArchSubDetail;
import com.arch.type.FieldType;

Essa definição anterior cria um CRUD mestre, detalhe com subdetalhe. Ao compilar o código todos os artefatos são gerados sendo eles:
- Entidades
- Fachada
Manager
- Observer
Actions
Páginas JSF
O framework já possuí nativamente controle de tenant por coluna, auditoria, multilinguas, controle de dados sigilosos, e muito mais. Possuí tambem diversas bibliotecas como por exemplo para consultas de JPA, geração de relatórios, entre outras...
A geração das anotações pode ser facilitada através de uma interface visual (GCA - Generate Code ARCH), essa ferramenta é desktop e foi desenvolvida utilizando JavaFX.
GCA

Conclusão

Para mais informações acesse JARCH Framework MVC.

Até mais...

Download package-info.java

JARCH com MAVEN

Introdução

Nesse Post vou mostrar as configurações necessárias para usar o JARCH Framework com um projeto no formato MAVEN. São 3 trechos de configurações dentro do arquivo pom.xml.

Configuração

A primeira configuração é para informar o projeto para utilizar os recursos de templates, css e javascripts.

1. Adicionar o plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <overlays>
            <overlay>
                <groupId>br.com.jarch</groupId>
                <artifactId>jarch-template</artifactId>
            </overlay>
        </overlays>
    </configuration>
</plugin>

A segunda configuração é para informar qual versão do framework estamos usando e para todas as dependências usadas pelo JARCH vamos usar a versão configurada no BOM.
2. Adicionar a dependência do BOM:

<dependencyManagement>
   <dependencies>        
        <dependency>
            <groupId>br.com.jarch</groupId>
            <artifactId>jarch-bom</artifactId>
            <version>18.6.0</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</dependencyManagement>

A terceira configuração é para adicionar todas as dependências do JARCH que no mímino para um projeto WEB são as informadas abaixo.
3. Adicionar as dependências:

<dependencies>
   <dependency>
      <groupId>br.com.jarch</groupId>
      <artifactId>jarch-framework</artifactId>
   </dependency>

   <dependency>
      <groupId>br.com.jarch</groupId>
      <artifactId>jarch-template</artifactId>
      <type>war</type>
   </dependency>
</dependencies>

Segue um exemplo de pom.xml usando o JARCH Framework.
Download do pom.xml

Conclusão

A configuração do JARCH Framework via MAVEN é muito simples, conforme mostrado nesse tutorial.

Até mais,

Versão 23.3.0-Final

      Introdução Nesse post vou mostrar as principais novidades da versão 23.3.0, algumas correções e pequenas alterações. Alterações Além d...