Módulos do guia

Nível 1 · Código · Módulo 04

Plugin e classe de modelo

Duração estimada: 2h a 3h

Você vai sair sabendo

  • →Entender por que toda customização no iDempiere vive num plugin OSGi
  • →Criar um plugin no Eclipse e subir junto com o servidor
  • →Fazer o plugin aplicar o 2Pack sozinho quando sobe
  • →Gerar as classes das suas tabelas e registrar uma model factory
  • →Escrever a primeira regra de negócio em Java, com beforeSave

Checkpoint: 0 de 6

ver .md

Antes de começar

  • Módulos 01 a 03 concluídos. Você vai precisar do 2Pack_1.0.0.zip gerado no módulo 03.
  • Servidor rodando em modo Debug pelo Eclipse (Run > Debug Configurations > server.product). A partir daqui, o debug deixa de ser opcional.
  • Este é o primeiro módulo com Java. O que se espera: saber criar classe, herança, e ler uma stack trace.

O que você vai construir

Até o módulo 03, a janela Visita Técnica funcionava sem nenhuma linha de Java. O iDempiere salvava os registros com uma classe genérica (GenericPO), que sabe gravar qualquer tabela mas não conhece nenhuma regra.

Neste módulo você cria o plugin com.gardenworld.visitas e coloca nele três coisas:

  • O 2Pack do módulo 03, aplicado automaticamente quando o plugin sobe. Instalar o plugin passa a instalar tabelas, janela e menu.
  • As classes das suas tabelas, geradas a partir do Dicionário.
  • A primeira regra de negócio: uma visita só pode ser concluída se tiver pelo menos uma linha.

Por que um plugin

O iDempiere roda sobre OSGi: o servidor é um conjunto de módulos (bundles) que sobem, param e se enxergam por contratos. O core é um desses bundles; o seu plugin é outro.

A regra da comunidade é simples: customização nunca altera o core. Tudo o que você escreve fica no seu plugin e conversa com o core por pontos de extensão (factories, event handlers, serviços). É isso que permite atualizar o iDempiere sem reaplicar customizações à mão.

Passo a passo

1. Crie o plugin

No Eclipse, File > New > Other > Plug-in Development > Plug-in Project.

CampoValor
Project namecom.gardenworld.visitas
Use default locationdesmarcado. Location: ~/sources/guia-idempiere-projeto/com.gardenworld.visitas
Target Platforman OSGi framework: Equinox
Execution environmentJavaSE-17

Na página seguinte:

CampoValor
Version1.0.0.qualifier
NameVisitas Técnicas
Generate an activatormarcado, com a classe com.gardenworld.visitas.Activator

Clique em Finish. O plugin fica dentro do repositório do projeto (o do módulo 03), mas aparece no workspace do Eclipse ao lado do core.

2. Declare as dependências

Abra META-INF/MANIFEST.MF, vá para a aba MANIFEST.MF (o texto puro) e deixe assim:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Visitas Técnicas
Bundle-SymbolicName: com.gardenworld.visitas;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: com.gardenworld.visitas.Activator
Bundle-RequiredExecutionEnvironment: JavaSE-17
Require-Bundle: org.adempiere.base;bundle-version="14.0.0",
 org.adempiere.plugin.utils;bundle-version="14.0.0"
Import-Package: org.osgi.framework,
 org.osgi.service.component.annotations;resolution:=optional
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: com.gardenworld.visitas

O que importa aqui:

  • Require-Bundle: o plugin enxerga as classes do core (org.adempiere.base) e os utilitários de plugin (org.adempiere.plugin.utils), onde fica o ativador que aplica 2Pack.
  • org.osgi.service.component.annotations: as anotações @Component que você vai usar no passo 6. Só existem na compilação, por isso resolution:=optional.
  • singleton:=true: só pode existir uma versão do plugin ativa por vez.

3. O plugin aplica o 2Pack sozinho

Mova o pacote do módulo 03 para dentro do plugin:

cd ~/sources/guia-idempiere-projeto
git mv 2pack/2Pack_1.0.0.zip com.gardenworld.visitas/META-INF/2Pack_1.0.0.zip

Troque o conteúdo de Activator.java:

package com.gardenworld.visitas;

import org.adempiere.plugin.utils.Incremental2PackActivator;

public class Activator extends Incremental2PackActivator {
}

Isso é tudo. Quando o plugin sobe, o Incremental2PackActivator procura arquivos META-INF/2Pack_*.zip, compara as versões com o que já foi instalado para este plugin e aplica só o que falta, em ordem de versão. O controle fica na tabela AD_Package_Imp, com o nome do plugin (com.gardenworld.visitas).

Quando mudar o Dicionário, gere 2Pack_1.0.1.zip e coloque ao lado. O 1.0.0 nunca é reaplicado.

4. Suba o plugin junto com o servidor

O server.product não inclui plugins novos sozinho.

  1. Run > Debug Configurations > server.product, aba Plug-ins.
  2. Na lista Workspace, marque com.gardenworld.visitas.
  3. Na coluna Auto-Start do plugin, escolha true.
  4. Clique em Validate Plug-ins. Não pode haver erro.
  5. Debug.

Sem o Auto-Start, o plugin é carregado mas não é iniciado: o ativador não roda e nada do que você registrar no passo 6 existe.

Com o servidor no ar, digite no console do Eclipse:

ss com.gardenworld

O plugin deve aparecer como ACTIVE. Se aparecer INSTALLED, faltou alguma dependência: rode diag <id> com o número da primeira coluna para ver qual.

Agora confira o 2Pack: entre como System, abra a janela Pack In e vá para a aba Package Installation. Deve haver um registro com.gardenworld.visitas, versão 1.0.0, com status de concluído.

5. Gere as classes das tabelas

Conceito: para cada tabela, o iDempiere trabalha com três classes:

ClasseQuem escreveO que tem
I_EDU_VisitageradaInterface com o nome da tabela, das colunas (COLUMNNAME_...) e as assinaturas dos getters e setters
X_EDU_VisitageradaImplementação dos getters e setters, herdando de PO (a classe que lê e grava no banco)
MVisitavocêHerda da X e concentra as regras de negócio

A separação existe porque as classes I e X são geradas de novo toda vez que uma coluna muda. Se você escrever regra nelas, perde tudo na próxima geração.

Primeiro, diga ao iDempiere qual é o pacote Java do tipo de entidade: como System, na janela Entity Type, abra EDU e preencha Model Package com com.gardenworld.visitas.model. O gerador usa isso quando uma tabela sua aponta para outra tabela sua.

Depois, rode o processo Generate Model (busque no menu):

ParâmetroValor
Foldero caminho absoluto da pasta src do plugin, por exemplo /home/voce/sources/guia-idempiere-projeto/com.gardenworld.visitas/src
DB Table NameEDU_%
Package Namecom.gardenworld.visitas.model
Table Entity TypeEDU
Column Entity Typeem branco (todas as colunas)
Generate Interfacemarcado
Generate Classmarcado

O processo roda no servidor e grava os arquivos na pasta informada, criando as subpastas do pacote. Como o servidor está na sua máquina, o caminho é o da sua máquina.

No Eclipse, clique no plugin e aperte F5 para ver os quatro arquivos novos: I_EDU_Visita, X_EDU_Visita, I_EDU_VisitaLinha e X_EDU_VisitaLinha.

Abra X_EDU_Visita e repare na primeira linha da classe:

@org.adempiere.base.Model(table="EDU_Visita")
public class X_EDU_Visita extends PO implements I_EDU_Visita, I_Persistent

É essa anotação que liga a classe à tabela. Repare também no nome dos getters: a coluna EDU_IsConcluida vira isEDU_IsConcluida(), e EDU_DataVisita vira getEDU_DataVisita().

6. Registre a model factory

O core ainda não sabe que essas classes existem. Quem diz ao iDempiere "para a tabela EDU_Visita, use esta classe" é uma model factory.

Crie com.gardenworld.visitas.VisitasModelFactory:

package com.gardenworld.visitas;

import org.adempiere.base.AnnotationBasedModelFactory;
import org.adempiere.base.IModelFactory;
import org.osgi.service.component.annotations.Component;

@Component(immediate = true, service = IModelFactory.class,
           property = {"service.ranking:Integer=1"})
public class VisitasModelFactory extends AnnotationBasedModelFactory {

    @Override
    protected String[] getPackages() {
        return new String[] {"com.gardenworld.visitas.model"};
    }
}

A classe base varre o pacote informado atrás de classes com @Model e monta o mapa tabela → classe. Quando encontra uma X que tem uma subclasse (a sua M), usa a subclasse.

O @Component registra a factory como serviço OSGi. O Eclipse transforma a anotação num arquivo em OSGI-INF/ e adiciona o cabeçalho Service-Component ao manifesto. Confira os dois depois de salvar.

7. A classe M e a primeira regra

Crie com.gardenworld.visitas.model.MVisita:

package com.gardenworld.visitas.model;

import java.sql.ResultSet;
import java.util.List;
import java.util.Properties;

import org.compiere.model.Query;

public class MVisita extends X_EDU_Visita {

    private static final long serialVersionUID = 1L;

    public MVisita(Properties ctx, int EDU_Visita_ID, String trxName) {
        super(ctx, EDU_Visita_ID, trxName);
    }

    public MVisita(Properties ctx, String EDU_Visita_UU, String trxName) {
        super(ctx, EDU_Visita_UU, trxName);
    }

    public MVisita(Properties ctx, ResultSet rs, String trxName) {
        super(ctx, rs, trxName);
    }

    /** Linhas da visita, em ordem. */
    public List<X_EDU_VisitaLinha> getLinhas() {
        return new Query(getCtx(), X_EDU_VisitaLinha.Table_Name,
                X_EDU_VisitaLinha.COLUMNNAME_EDU_Visita_ID + "=?", get_TrxName())
            .setParameters(getEDU_Visita_ID())
            .setOrderBy(X_EDU_VisitaLinha.COLUMNNAME_Line)
            .list();
    }

    @Override
    protected boolean beforeSave(boolean newRecord) {
        if (isEDU_IsConcluida()
                && is_ValueChanged(COLUMNNAME_EDU_IsConcluida)
                && getLinhas().isEmpty()) {
            log.saveError("Error", "Inclua pelo menos uma linha antes de concluir a visita");
            return false;
        }
        return true;
    }
}

Pontos que valem para qualquer classe M:

  • Os três construtores são obrigatórios. A factory cria os objetos por reflexão: pelo ID, pelo UUID e a partir de uma linha de ResultSet. Faltou um, e algum caminho do core falha ao criar o objeto.
  • beforeSave devolve false para barrar o salvamento. O log.saveError define a mensagem que o usuário vê.
  • get_TrxName() em toda consulta. A consulta roda dentro da mesma transação do registro sendo salvo. Sem isso, você lê o banco "de fora" e não enxerga o que ainda não foi confirmado.
  • is_ValueChanged evita que a regra dispare em todo salvamento. Aqui ela só roda quando alguém marca Concluída.

O nome segue o costume do core: a classe M tira o prefixo da tabela (X_C_Order → MOrder). Uma M por tabela é o normal; a EDU_VisitaLinha não precisa de uma por enquanto.

8. Teste

O Eclipse recompila ao salvar e, em modo Debug, costuma aplicar a mudança sem reiniciar. Para uma classe nova e uma factory nova, reinicie o servidor.

  1. Como GardenAdmin, crie uma visita, sem linhas, e salve.
  2. Marque Concluída e salve. A mensagem da sua regra aparece e o registro não é gravado.
  3. Desmarque, inclua uma linha, volte ao cabeçalho, marque Concluída e salve. Agora grava.

Por fim, coloque um breakpoint na primeira linha do beforeSave e salve qualquer visita. O Eclipse para ali. Inspecione this: é um MVisita, não um GenericPO. A factory está funcionando.

9. Commit

cd ~/sources/guia-idempiere-projeto
git add .
git commit -m "Plugin com.gardenworld.visitas: 2Pack, classes de modelo e regra de conclusão"

Não versione a pasta bin/ nem target/: crie um .gitignore com as duas.

Checkpoint

0 de 6 itens

Erros comuns

SintomaCausa provávelCorreção
ss mostra o plugin como INSTALLEDDependência não resolvidadiag <id> no console mostra qual; confira o Require-Bundle
O plugin nem aparece no ssNão foi marcado no server.productPasso 4
ACTIVE, mas o 2Pack não foi aplicadoO zip não está em META-INF/ ou o nome não segue 2Pack_x.y.z.zipConfira o nome e o local; veja o log do servidor procurando com.gardenworld.visitas
O Generate Model termina sem gerar nadaDB Table Name ou Table Entity Type errado; o filtro diferencia maiúsculasUse EDU_% e EDU, exatamente
Source folder doesn't existsCaminho relativo, com ~ ou com erro de digitaçãoUse o caminho absoluto da pasta src
A regra não dispara e o breakpoint não paraA factory não foi registradaConfira OSGI-INF/ e o Service-Component no manifesto; confira o Auto-Start
A regra dispara em todo salvamentoFaltou is_ValueChangedPasso 7

Desafio

No módulo 02, a Mandatory Logic obriga o Motivo da Urgência quando a visita é urgente. Mas essa lógica é da tela: uma visita criada por um processo, uma importação ou uma API passa direto por ela.

Repita a regra no beforeSave da MVisita, para que valha em qualquer caminho. Dica: isEDU_IsUrgente() e getEDU_MotivoUrgencia().

Referência

Eventos da classe de modelo

MétodoQuando rodaUso típico
beforeSave(boolean newRecord)Antes do INSERT/UPDATEValidar e completar campos. false barra o salvamento
afterSave(boolean newRecord, boolean success)Depois do INSERT/UPDATE, na mesma transaçãoAtualizar registros relacionados
beforeDelete()Antes do DELETEImpedir exclusão
afterDelete(boolean success)Depois do DELETELimpar registros relacionados

Os quatro rodam antes dos event handlers de outros plugins (módulo 05). Regra da própria tabela fica na M; reação a tabelas de outros (inclusive do core) fica em event handler.

Métodos úteis de PO

MétodoPara quê
is_ValueChanged(coluna)A coluna mudou neste salvamento?
get_ValueOld(coluna)Valor antes da mudança
is_new()Registro ainda não gravado
get_TrxName()Transação atual, para repassar a consultas e a outros objetos
saveEx()Salva e lança exceção em caso de erro (prefira a save() no seu código)

Alternativa: o model.generator do Eclipse

O workspace do iDempiere tem uma configuração de execução model.generator (Run > Run Configurations > Eclipse Application) que gera as mesmas classes sem subir o servidor. O processo Generate Model, usado no passo 5, existe desde o iDempiere 11 e dispensa essa configuração.

Leitura oficial

Próximo módulo

05 · Regras de negócio: callout e event handler: reagir ao que o usuário digita, antes de salvar, e ao que outros plugins e o core gravam.