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
Antes de começar
- Módulos 01 a 03 concluídos. Você vai precisar do
2Pack_1.0.0.zipgerado 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.
| Campo | Valor |
|---|---|
| Project name | com.gardenworld.visitas |
| Use default location | desmarcado. Location: ~/sources/guia-idempiere-projeto/com.gardenworld.visitas |
| Target Platform | an OSGi framework: Equinox |
| Execution environment | JavaSE-17 |
Na página seguinte:
| Campo | Valor |
|---|---|
| Version | 1.0.0.qualifier |
| Name | Visitas Técnicas |
| Generate an activator | marcado, 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@Componentque você vai usar no passo 6. Só existem na compilação, por issoresolution:=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.
- Run > Debug Configurations > server.product, aba Plug-ins.
- Na lista Workspace, marque
com.gardenworld.visitas. - Na coluna Auto-Start do plugin, escolha
true. - Clique em Validate Plug-ins. Não pode haver erro.
- 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:
| Classe | Quem escreve | O que tem |
|---|---|---|
I_EDU_Visita | gerada | Interface com o nome da tabela, das colunas (COLUMNNAME_...) e as assinaturas dos getters e setters |
X_EDU_Visita | gerada | Implementação dos getters e setters, herdando de PO (a classe que lê e grava no banco) |
MVisita | você | 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âmetro | Valor |
|---|---|
| Folder | o caminho absoluto da pasta src do plugin, por exemplo /home/voce/sources/guia-idempiere-projeto/com.gardenworld.visitas/src |
| DB Table Name | EDU_% |
| Package Name | com.gardenworld.visitas.model |
| Table Entity Type | EDU |
| Column Entity Type | em branco (todas as colunas) |
| Generate Interface | marcado |
| Generate Class | marcado |
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. beforeSavedevolvefalsepara barrar o salvamento. Olog.saveErrordefine 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_ValueChangedevita 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.
- Como GardenAdmin, crie uma visita, sem linhas, e salve.
- Marque Concluída e salve. A mensagem da sua regra aparece e o registro não é gravado.
- 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
Erros comuns
| Sintoma | Causa provável | Correção |
|---|---|---|
ss mostra o plugin como INSTALLED | Dependência não resolvida | diag <id> no console mostra qual; confira o Require-Bundle |
O plugin nem aparece no ss | Não foi marcado no server.product | Passo 4 |
ACTIVE, mas o 2Pack não foi aplicado | O zip não está em META-INF/ ou o nome não segue 2Pack_x.y.z.zip | Confira o nome e o local; veja o log do servidor procurando com.gardenworld.visitas |
| O Generate Model termina sem gerar nada | DB Table Name ou Table Entity Type errado; o filtro diferencia maiúsculas | Use EDU_% e EDU, exatamente |
Source folder doesn't exists | Caminho relativo, com ~ ou com erro de digitação | Use o caminho absoluto da pasta src |
| A regra não dispara e o breakpoint não para | A factory não foi registrada | Confira OSGI-INF/ e o Service-Component no manifesto; confira o Auto-Start |
| A regra dispara em todo salvamento | Faltou is_ValueChanged | Passo 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étodo | Quando roda | Uso típico |
|---|---|---|
beforeSave(boolean newRecord) | Antes do INSERT/UPDATE | Validar e completar campos. false barra o salvamento |
afterSave(boolean newRecord, boolean success) | Depois do INSERT/UPDATE, na mesma transação | Atualizar registros relacionados |
beforeDelete() | Antes do DELETE | Impedir exclusão |
afterDelete(boolean success) | Depois do DELETE | Limpar 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étodo | Para 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
- Plugin: Model Factory (em inglês)
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.