O “Arroba Tela”

A intenção desse artigo é descrever a nova estrutura para construção de componentes de formulários desenvolvido no NAVE, entre agosto e setembro de 2014.

O problema

Apesar de que o guia de boas práticas do wicket recomenda fortemente a não utilização de uma fábrica de componentes (http://wicket.apache.org/guide/guide/bestpractices.html#bestpractices_12), acreditamos que é uma boa estratégia para nós. Com a fábrica, podemos padronizar os componentes que utilizamos, já que cada forma de entrada (ou cada FormComponent) tem uma particularidade nossa.

O maior argumento de não utilizar uma fábrica de componentes, é o fato de não conseguir recuperá-los de forma transparente no componente necessário, ou mesmo incidir ações sobre ele (como eventos - change, blur, ou alterar alguma característica em tempo de execução).

Com a nossa antiga estrutura de fábrica de componentes (jmine.tec.web.wicket.component.injection.FormInputProvider), resolvemos o problema da padronização, mas os outros problemas continuavam, impedindo o uso contínuo dessa fábrica.

Visão geral da solução

Utilizando o conceito de fábrica, a solução é baseada em anotações que vão criar componentes específicos, dependendo do tipo do atributo que for anotado.

A anotação será feita no ModelObject (MO) da tela. Cada tela (ou painel), deve especificamente ter seu próprio MO, que é o responsável por criar os campos da tela.

../../_images/estrutura-crud.png

Existem anotações para a grande maioria dos componentes e ações que precisamos atualmente. Essas anotações são basicamente de dois tipos: fábrica e decoração (sendo que decorações podem ser de layout ou de eventos). As anotações de fábrica constróem componentes, e as anotações de decoração alteram o layout dos componentes.

Detalhes da solução

Anotações existentes:

Anotações de fábrica:

  • AutoComplete
  • FormInput
  • DateInput
  • MultiChoice

Anotações de decoração de layout:

  • Label
  • Enabled
  • Visible
  • Width
  • Length

Anotações de decoração de eventos:

  • OnBlur
  • OnChange
  • OnEvent

Vantagens e desvantagens

Vantagens

  • Os MO’s não precisam mais de objetos persistíveis.
  • Testes para as telas ficam mais simples. Testes unitários para os eventos.

Desvantagens

  • É necessário especificar o tipo do input no HTML.
  • O código pode ficar mais verboso.

Exemplos de uso

Note

Colocar um exemplo mais simples de uso das anotações.

public class WebTestEntity implements Persistable, Serializable {

    private Long pk;

    private boolean dirty = false;

    private String nome;

    private String mnemonico;

    private BigDecimal valor;

    private BigDecimal valorPercentual;

    private Date data;

    private Date dataFim;

    private Timestamp timestamp;

    private WebTestEntityEnum webTestEntityEnum;

    private WebTestEntity parent;

    private Set<WebTestEntity> children = new HashSet<WebTestEntity>();

    /**
     * Construtor.
     */
    public WebTestEntity() {
        super();
    }

    /**
     * Construtor.
     *
     * @param pk pk
     * @param nome nome
     * @param mnemonico mnemonico
     * @param valor valor
     * @param data data
     */
    public WebTestEntity(Long pk, String nome, String mnemonico, BigDecimal valor, Date data) {
        this(pk, nome, mnemonico, valor, data, null, null, null);
    }

    /**
     * Construtor.
     *
     * @param pk pk
     * @param nome nome
     * @param mnemonico mnemonico
     * @param valor valor
     * @param data data
     */
    public WebTestEntity(Long pk, String nome, String mnemonico, BigDecimal valor, Date data, Date dataFim) {
        this(pk, nome, mnemonico, valor, data, dataFim, null, null);
    }

    /**
     * Construtor.
     *
     * @param pk pk
     * @param nome nome
     * @param mnemonico mnemonico
     * @param valor valor
     * @param data data
     * @param parent pai
     */
    public WebTestEntity(Long pk, String nome, String mnemonico, BigDecimal valor, Date data, Date dataFim, WebTestEntity parent) {
        this(pk, nome, mnemonico, valor, data, dataFim, null, parent);
    }

    /**
     * Construtor.
     *
     * @param pk pk
     * @param nome nome
     * @param mnemonico mnemonico
     * @param valor valor
     * @param data data
     * @param timestamp timestamp
     */
    public WebTestEntity(Long pk, String nome, String mnemonico, BigDecimal valor, Date data, Date dataFim, Timestamp timestamp) {
        this(pk, nome, mnemonico, valor, data, dataFim, timestamp, null);
    }

    /**
     * Construtor.
     *
     * @param pk pk
     * @param nome nome
     * @param mnemonico mnemonico
     * @param valor valor
     * @param data data
     * @param timestamp timestamp
     * @param parent pai
     */
    public WebTestEntity(Long pk, String nome, String mnemonico, BigDecimal valor, Date data, Date dataFim, Timestamp timestamp,
            WebTestEntity parent) {
        super();
        this.pk = pk;
        this.nome = nome;
        this.mnemonico = mnemonico;
        this.valor = valor;
        this.data = data;
        this.dataFim = dataFim;
        this.timestamp = timestamp;
        this.parent = parent;
    }

    /**
     * @return the pk
     */
    public Long getPk() {
        return this.pk;
    }

    /**
     * @param pk the pk to set
     */
    public void setPk(Serializable pk) {
        this.pk = (Long) pk;
    }

    /**
     * @return the nome
     */
    @DisplayName(value = "Nome", order = "1")
    public String getNome() {
        return this.nome;
    }

    /**
     * @param nome the nome to set
     */
    public void setNome(String nome) {
        this.nome = nome;
    }

    /**
     * @return the mnemonico
     */
    @NaturalKey
    @DisplayName(value = "Mnemônico", order = "0")
    public String getMnemonico() {
        return this.mnemonico;
    }

    /**
     * @param mnemonico the mnemonico to set
     */
    public void setMnemonico(String mnemonico) {
        this.mnemonico = mnemonico;
    }

    /**
     * @return the valor
     */
    @DisplayName(value = "Valor", order = "2")
    public BigDecimal getValor() {
        return this.valor;
    }

    /**
     * @param valor the valor to set
     */
    public void setValor(BigDecimal valor) {
        this.valor = valor;
    }

    /**
     * @return the valor percentual
     */
    @DisplayName(value = "Valor", order = "3")
    public BigDecimal getValorPercentual() {
        return this.valorPercentual;
    }

    /**
     * @param valorTotal the total valor of the table
     */
    public void setValorPercentual(BigDecimal valorTotal) {
        if (valorTotal.abs().floatValue() < 0.0001) {
            this.valorPercentual = new BigDecimal(0);
        } else {
            this.valorPercentual = this.valor.divide(valorTotal, 10, RoundingMode.HALF_UP);
        }
    }

    /**
     * @return the date
     */
    @DisplayName(value = "Data", order = "5")
    public Date getData() {
        return this.data;
    }

    /**
     * @param data the data to set
     */
    public void setData(Date data) {
        this.data = data;
    }

    /**
     * @return the dataFim
     */
    public Date getDataFim() {
        return this.dataFim;
    }

    /**
     * @param dataFim the dataFim to set
     */
    public void setDataFim(Date dataFim) {
        this.dataFim = dataFim;
    }

    /**
     * @return the timestamp
     */
    public Timestamp getTimestamp() {
        return this.timestamp;
    }

    /**
     * @param timestamp the timestamp
     */
    public void setTimestamp(Timestamp timestamp) {
        this.timestamp = timestamp;
    }

    /**
     * @return the webTestEntityEnum
     */
    public WebTestEntityEnum getWebTestEntityEnum() {
        return this.webTestEntityEnum;
    }

    /**
     * @param webTestEntityEnum the webTestEntityEnum to set
     */
    public void setWebTestEntityEnum(WebTestEntityEnum webTestEntityEnum) {
        this.webTestEntityEnum = webTestEntityEnum;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return (new ReflectionToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) {
            @Override
            protected boolean accept(java.lang.reflect.Field f) {
                return super.accept(f) && !f.getName().equals("parent");
            }
        }).toString();
    }

    /**
     * @return the parent
     */
    @DisplayName(value = "Pai", order = "10")
    public WebTestEntity getParent() {
        return this.parent;
    }

    /**
     * @param parent the parent to set
     */
    public void setParent(WebTestEntity parent) {
        this.parent = parent;
    }

    /**
     * @return
     */
    @DisplayName(value = "Filhos", order = "20")
    public Set<WebTestEntity> getChildren() {
        return this.children;
    }

    /**
     * @param child this entity children
     */
    public void addChild(WebTestEntity child) {
        this.children.add(child);
    }

    /**
     * {@inheritDoc}
     */
    public Long getId() {
        return this.pk;
    }

    public boolean isDirty() {
        return this.dirty;
    }

    public void setDirty(boolean flag) {
        this.dirty = flag;
    }

    public String identification() {
        return this.getMnemonico();
    }
}
public class WebTestEntityMO implements Serializable {

    private String nome;

    private String mnemonico;

    private BigDecimal valor;

    private BigDecimal valorPercentual;

    private Date data;

    private Date dataFim;

    private WebTestEntityEnum webTestEntityEnum;

    private WebTestEntityEnum webTestEntityEnumFull;

    private String parImpar;

    private String parent;

    private Collection<String> children;

    /**
     * @return the nome
     */
    public String getNome() {
        return this.nome;
    }

    /**
     * @param nome the nome to set
     */
    @Required
    @FormInput
    @Length(5)
    @Label("Nome (@Required, @FormInput, @Length)")
    public void setNome(String nome) {
        this.nome = nome;
    }

    /**
     * @return the mnemonico
     */
    public String getMnemonico() {
        return this.mnemonico;
    }

    /**
     * @param mnemonico the mnemonico to set
     */
    @FormInput
    @Length(15)
    @Label("Mnemônico (@FormInput, @Length)")
    public void setMnemonico(String mnemonico) {
        this.mnemonico = mnemonico;
    }

    /**
     * @return the valor
     */
    public BigDecimal getValor() {
        return this.valor;
    }

    /**
     * @param valor the valor to set
     */
    @FormInput
    @Width
    @Label("Valor (@FormInput, @Width)")
    public void setValor(BigDecimal valor) {
        this.valor = valor;
    }

    /**
     * @return the valorPercentual
     */
    public BigDecimal getValorPercentual() {
        return this.valorPercentual;
    }

    /**
     * @param valorPercentual the valorPercentual to set
     */
    @FormInput
    @Width(BootstrapInputWidth.MINI)
    @Addon("%")
    @Label("Valor percentual (@FormInput, @Width, @Addon)")
    public void setValorPercentual(BigDecimal valorPercentual) {
        this.valorPercentual = valorPercentual;
    }

    /**
     * @return the data
     */
    public Date getData() {
        return this.data;
    }

    /**
     * @param data the data to set
     */
    @DateInput
    @Label("Data (@DateInput)")
    public void setData(Date data) {
        this.data = data;
    }

    /**
     * @return the dataFim
     */
    public Date getDataFim() {
        return this.dataFim;
    }

    /**
     * @param dataFim the dataFim to set
     */
    @DateInput
    @Label("Data fim (@DateInput)")
    public void setDataFim(Date dataFim) {
        this.dataFim = dataFim;
    }

    /**
     * @return the webTestEntityEnum
     */
    public WebTestEntityEnum getWebTestEntityEnum() {
        return this.webTestEntityEnum;
    }

    /**
     * @param webTestEntityEnum the webTestEntityEnum to set
     */
    @Required
    @DropDown(dataProvider = WebTestEntityEnumDataProvider.class)
    @Width
    @Label("Enum (@Required, @DropDown, @Width)")
    public void setWebTestEntityEnum(WebTestEntityEnum webTestEntityEnum) {
        this.webTestEntityEnum = webTestEntityEnum;
    }

    /**
     * @return the webTestEntityEnumFull
     */
    public WebTestEntityEnum getWebTestEntityEnumFull() {
        return this.webTestEntityEnumFull;
    }

    /**
     * @param webTestEntityEnumFull the webTestEntityEnumFull to set
     */
    @DropDown(dataProvider = EnumDropDownDataProvider.class, required = true)
    @Width
    @Label("Enum - Todas as opções (@DropDown, @Width)")
    public void setWebTestEntityEnumFull(WebTestEntityEnum webTestEntityEnumFull) {
        this.webTestEntityEnumFull = webTestEntityEnumFull;
    }

    /**
     * @return the parImpar
     */
    public String getParImpar() {
        return this.parImpar;
    }

    /**
     * @param parImpar the parImpar to set
     */
    @DropDown(dataProvider = WebTestEntityParImparDataProvider.class)
    @Width
    @OnChange
    @Label("Par/Ímpar (@DropDown, @Width, @OnChange)")
    public void setParImpar(String parImpar) {
        this.parImpar = parImpar;
    }

    /**
     * @return the parent
     */
    public String getParent() {
        return this.parent;
    }

    /**
     * @param parent the parent to set
     */
    @AutoComplete(targetType = WebTestEntity.class, required = true, dataProvider = WebTestEntityEvenOddDataProvider.class)
    @Width(BootstrapInputWidth.LARGE)
    @Label("WebTestEntities (@AutoComplete, @Width)")
    public void setParent(String parent) {
        this.parent = parent;
    }

    /**
     * @return the children
     */
    public Collection<String> getChildren() {
        return this.children;
    }

    /**
     * @param children the children to set
     */
    @MultiChoice(targetType = WebTestEntity.class, required = true, dataProvider = WebTestEntityDataProvider.class)
    @Width(BootstrapInputWidth.XXLARGE)
    @Label("WebTestEntities (@MultiChoice, @Width)")
    public void setChildren(Collection<String> children) {
        this.children = children;
    }

}
public class WebTestEntityPage extends Template {

    /**
     * Construtor.
     */
    public WebTestEntityPage() {
        this.add(new WebTestEntityPanel("panel"));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected MessageCreator getHelpTextCreator() {
        return new MockMessageCreator();
    }

}
public class WebTestEntityPanel extends Panel {

    @Injected
    private FormComponentInjector injector;

    /**
     * Construtor.
     * 
     * @param id identificador do wicket.
     */
    public WebTestEntityPanel(String id) {
        super(id);

        Form<WebTestEntity> form = new Form<WebTestEntity>("mainForm");

        this.injector.inject(form, new WebTestEntityMO());

        this.add(form);
    }

}

DataProviders

public class WebTestEntityEvenOddDataProvider extends EntityAutoCompleteDataProvider {

    @Injected
    private WebTestEntityDAO dao;

    /**
     * {@inheritDoc}
     */
    @Override
    protected List<PersistableVO> doGetData(DAO<? extends Persistable> entityDAO, String term, int maxResults, Serializable inputProvider) {
        List<PersistableVO> result = new ArrayList<PersistableVO>();
        List<WebTestEntity> entities = null;
        if (((WebTestEntityMO) inputProvider).getParImpar() == null) {
            return this.dao.findVOByLikeNaturalKeyWithMaxResults(term, maxResults);
        } else {
            boolean isPar = ((WebTestEntityMO) inputProvider).getParImpar().equals("Par");
            if (isPar) {
                entities = this.dao.findByLikeMneEvenPk(term, maxResults);
            } else {
                entities = this.dao.findByLikeMneOddPk(term, maxResults);
            }
        }
        for (WebTestEntity entity : entities) {
            result.add(new PersistableVO(entity.getPk(), entity.getMnemonico(), entity.getClass()));
        }
        return result;
    }

    @Override
    protected DAO<? extends Persistable> getDAO(Class<?> targetClass) {
        return this.dao;
    }

    /**
     * @param dao the dao to set
     */
    public void setDao(WebTestEntityDAO dao) {
        this.dao = dao;
    }

}

Testando DataProviders

public class WebTestEntityEvenOddDataProviderTest {

    private WebTestEntityEvenOddDataProvider dataProvider;

    @Before
    public void before() {
        this.dataProvider = new WebTestEntityEvenOddDataProvider();
        WebTestEntityDAO dao = new WebTestEntityDAO();
        new MockInjected().wire(dao);
        this.dataProvider.setDao(dao);
    }

    /**
     * Verifica os valores exibidos para todos os resultados.
     */
    @Test
    public void testaTodosOsValoresExibidos() {
        WebTestEntityMO inputProvider = new WebTestEntityMO();

        List<PersistableVO> data = this.dataProvider.getData("", 6, WebTestEntity.class, inputProvider);

        assertEquals(6, data.size());
        assertEquals("Mnemonico 0", data.get(0).getNaturalKey());
        assertEquals("Mnemonico 1", data.get(1).getNaturalKey());
        assertEquals("Mnemonico 2", data.get(2).getNaturalKey());
        assertEquals("Mnemonico 3", data.get(3).getNaturalKey());
        assertEquals("Mnemonico 4", data.get(4).getNaturalKey());
        assertEquals("Mnemonico 5", data.get(5).getNaturalKey());
    }

    /**
     * Verifica se definindo o valor "Par", o data provider só traz resultados pares.
     */
    @Test
    public void testaValoresPares() {
        WebTestEntityMO inputProvider = new WebTestEntityMO();
        inputProvider.setParImpar("Par");

        List<PersistableVO> data = this.dataProvider.getData("", 50, WebTestEntity.class, inputProvider);

        assertEquals(50, data.size());
        assertEquals("Mnemonico 0", data.get(0).getNaturalKey());
        assertEquals("Mnemonico 2", data.get(1).getNaturalKey());
        assertEquals("Mnemonico 4", data.get(2).getNaturalKey());
    }

    /**
     * Verifica se definindo o valor "Ímpar", o data provider só traz resultados ímpares.
     */
    @Test
    public void testaValoresImpares() {
        WebTestEntityMO inputProvider = new WebTestEntityMO();
        inputProvider.setParImpar("Ímpar");

        List<PersistableVO> data = this.dataProvider.getData("", 50, WebTestEntity.class, inputProvider);

        assertEquals(50, data.size());
        assertEquals("Mnemonico 1", data.get(0).getNaturalKey());
        assertEquals("Mnemonico 3", data.get(1).getNaturalKey());
        assertEquals("Mnemonico 5", data.get(2).getNaturalKey());
    }

    /**
     * Garante que o choice converter do data provider é {@link SameTypeChoiceConverter}.
     */
    @Test
    public void testChoiceConverter() {
        assertThat((SameTypeChoiceConverter) this.dataProvider.getChoiceConverter(), isA(SameTypeChoiceConverter.class));
    }

    /**
     * Garante que o choice renderer do data provider é {@link AutoCompleteChoiceRenderer}.
     */
    @Test
    public void testChoiceRenderer() {
        assertThat((AutoCompleteChoiceRenderer) this.dataProvider.getChoiceRenderer(), isA(AutoCompleteChoiceRenderer.class));
    }
}

Páginas de formulário

Note

Colocar exemplo de uma página de formulário.

Páginas de visualização

Note

Colocar exemplo de uma página de pesquisa/consulta.