Cockpit

O Cockpit é uma interface voltada ao usuário, permitindo que este possa construir o próprio fluxo de uso através de diferentes painéis. É uma tela dinâmica, sendo necessária a utilização de navegadores mais atualizados.

Estrutura de componentes

A tela do Cockpit continua sendo uma tela Wicket, por isso deve obedecer a hierarquia de componentes tanto no código Java quanto no markup.

Página do Cockpit

A página do Cockpit (CockpitPage) é o principal holder de todos os outros componentes que constroem o Cockpit. Nela é configurável todos os componentes e estruturas de componentes.

Configurando a página

Como visto no artigo Configurando estilos numa aplicação Jmine, é possível configurar o estilo do Cockpit, tal como os painéis que serão adicionados à página e o estilo que será aplicado sobre a mesma através da classe CockpitStyleConfig (jmine.tec.web.wicket.cockpit.page.CockpitStyleConfig).

Exemplo de configuração do estilo do Cockpit:

<bean id="cockpitStyleConfig" class="jmine.tec.web.wicket.cockpit.page.CockpitStyleConfig">
                <constructor-arg>
                        <list>
                                <ref bean="headerInitializer" />
                                <ref bean="templateHeaderInitializer" />
                        </list>
                </constructor-arg>
                <constructor-arg>
                        <ref bean="cockpitPanelsFactory" />
                </constructor-arg>
</bean>

Os painéis adicionados na página do Cockpit, o header, o footer e o menu do Cockpit são definidos pela interface CockpitPanelsFactory (CockpitPanelsFactory).

Exemplo da implementação da factory:

public class DefaultCockpitPanelsFactory implements CockpitPanelsFactory {

                public Panel createHeaderPanel(String id,
                                SecurityService securityService) {
                        return new EmptyPanel(id);
                }

                public Panel createFooterPanel(String id) {
                        return new EmptyPanel(id);
                }

                public Panel createCockpitMenuPanel(String id,
                                CockpitMenuCreator cockpitMenuCreator) {
                        return new EmptyPanel(id);
                }

}

Após a interface ser implementada, lembrar de escrever um bean da factory junto com a configuração de estilos do Cockpit.

Exemplo:

<bean id="cockpitPanelsFactory"
                class="path.to.DefaultCockpitPanelsFactory">
</bean>

Lembrando que a configuração do estilo do Cockpit deve ser adicionada ao estilo do qual ela faz parte.

Exemplo:

<bean id="myStyle" lazy-init="false"
        class="jmine.tec.web.wicket.styles.Style">
        <property name="styleName" value="myStyle"></property>
        <property name="menuCreator" ref="menuCreator" />
        <property name="styleConfigs">
                <list>
                        <ref bean="templateConfig" />
                        <ref bean="loginConfig" />
                        <ref bean="cockpitStyleConfig" />
                </list>
        </property>
</bean>

Containers de painéis

Os containers de painel são responsáveis por armazenar painéis do Cockpit. Deve armazenar a lista de configurações de painéis que é responsável para exibir.

Exemplo:

public class DefaultCockpitPanelContainer extends CockpitPanelContainer {

public DefaultCockpitPanelContainer(String id) {
        super(id);
}

@Override
protected List<CockpitPanelConfiguration> loadPanelConfigurations() {
        return new ArrayList<CockpitPanelConfiguration>();
}

@Override
public String getKey() {
        return "defaultContainer";
}

}

O método CockpitPanelContainer#loadPanelConfigurations() é responsável por retornar todas as configurações que serão utilizadas para construir os painéis dentro do container. O método CockpitPanelContainer#getKey() retornará a chave de identificação do container.

A página do Cockpit possui já definidos ao menos dois containers para armazenar estes painéis:

Painéis Fixados

Os painéis adicionados no container de painéis fixados (AbstractAttachedCockpitPanelContainer) serão organizados em um grid. Eles podem ser reordenados da maneira que o usuário preferir. Há uma implementação base para este container, o AttachedCockpitPanelContainer (AttachedCockpitPanelContainer), que além de obter todas as configurações que estejam definidas como “fixas”, adiciona o comportamento de ordenação ao grid com os painéis.

Painéis Soltos

Os painéis adicionados no container de painéis soltos (AbstractDetachedCockpitPanelContainer) serão organizados pela janela do browser. Eles podem ser soltos em qualquer parte da janela do browser, inclusive sobre outros painéis. Há uma implementação base para este container, o DetachedCockpitPanelContainer (DetachedCockpitPanelContainer), que além de obter todas as configurações que estejam definidas como “soltas”, adiciona o comportamento de arrastar e soltar aos painéis criados neste container.

Painel Maximizado

Fora de qualquer container, pode existir ou não um painel maximizado. este painel ocupará todo o espaço da janela do browser, semelhante a uma janela sendo maximizada no sistema. Este painel é definido pela própria página do Cockpit, que buscará a configuração do único painel (se houver um) do painel que está maximizado.

Painel do Cockpit

O painel do Cockpit é um holder para outros painéis que conterá o conteúdo útil para o próprio painel. Basicamente é um painel em forma de janela.

Através da configuração do painel do Cockpit informada, consegue criar o conteúdo e definir quais ações podem ser tomadas sobre o próprio painel.

Ações de um painel

As ações de um painel são as ações que podem ser tomadas sobre uma janela do Cockpit. As ações são definidas pelo painel CockpitPanelActionPanel (jmine.tec.web.wicket.cockpit.component.panel.action.CockpitPanelActionPanel). Este painel esta presente na barra de cabeçalho de um painel do Cockpit. Sabe construir todos os itens com as ações através de uma lista de CockpitPanelActionCommand (CockpitPanelActionCommand).

A interface CockpitPanelActionCommand é responsável por informar a ação que será executada ao clicar em seu item, qual ícone será exibido para essa ação e qual o nome da ação.

Exemplo de implementação:

public class AnActionCommand implements CockpitPanelActionCommand {

public String getTitle() {
        return "An Action";
}

public WebMarkupContainer createAction(String id) {
        return new AjaxLink<Void>(id){
                @Override
                public void onClick(AjaxRequestTarget target) {
                        this.doSomeLogic();
                }
        };
}

public Component createImage(String id) {
        return new Image(id, new PackageResourceReference(ImageResources.class, "image.png"));
}

}

Existem já alguns comandos pré-definidos:

  • AttachCockpitPanelCommand (AttachCockpitPanelCommand), responsável por fixar um painel de volta ao grid;
  • CloseCockpitPanelCommand (CloseCockpitPanelCommand), responsável por fechar o painel respectivo;
  • DetachCockpitPanelCommand (DetachCockpitPanelCommand), responsável por tornar o painel solto;
  • MaximizeCockpitPanelCommand (MaximizeCockpitPanelCommand), responsável por tornar o painel maximizado na janela no Cockpit;
  • RestoreCockpitPanelCommand (RestoreCockpitPanelCommand), responsável por restaurar o painel maximizado para seu estado anterior.

As ações definidas num painel podem ser definidas através do método CockpitPanel#getPanelActions().

Conteúdo de um painel

O conteúdo de um painel é definido por sua configuração (CockpitPanelConfiguration). Através de um conversor do painel (ICockpitPanelConverter) consegue criar o componente que exibirá todo o conteúdo do painel.

Configurações de um painel

A configuração de um painel armazena todas as informações sensíveis para a construção e atualização de um painel, tal como a classe do painel de conteúdo, o tamanho da janela, o estado da janela (fixado, solto ou maximizado) e os parâmetros utilizados na construção da janela.

Pode ser construído com um builder auxiliar CockpitPanelConfigurationBuilder (CockpitPanelConfigurationBuilder).

Parâmetros de um painel

Os parâmetros de um painel são quaisquer informações necessárias utilizadas na construção do painel. Eles são armazenados em um mapa, onde qualquer parâmetro pode ser recuperado através de sua chave.

Existem dois builders auxiliares para a construção de cada parâmetro, sendo eles:

  • CockpitPanelParametersBuilder (jmine.tec.web.wicket.cockpit.builder.CockpitPanelParametersBuilder), responsável por criar e armazenar o mapa de parâmetros.
  • CockpitPanelParameterBuilder (jmine.tec.web.wicket.cockpit.builder.CockpitPanelParameterBuilder), responsável por criar um parâmetro com sua relação chave-valor.

Conversor de painel

Para que um painel seja construído, é necessário que ele conheça de qual maneira seu conteúdo será descrito. Para isso, os conversores (ICockpitPanelConverter) são capazes de filtrar os parâmetros e transformá-los em um painel e vice-versa.

Exemplo de implementação:

public class APanelConverter implements CockpitPanelConverter<EmptyPanel>{

                public EmptyPanel convertToPanel(String id,
                                CockpitPanelParameters parameters) {
                        return new EmptyPanel(id);
                }

                public CockpitPanelParameters convertToParameters(EmptyPanel panel) {
                        return new CockpitPanelParameters();
                }

                public Class<EmptyPanel> getTargetClass() {
                        return EmptyPanel.class;
                }
}

Ainda, é necessária a criação de um bean responsável pelo registro dos conversores, cujo receiver é a classe CockpitPanelRegistry (CockpitPanelRegistry). Assim, os conversores podem ser acessados através da classe do painel.

Exemplo de implementação:

<bean id="somePanelConverters" class="jmine.tec.utils.register.Registrar">
        <property name="receiver" ref="cockpitPanelRegistry" />
        <property name="registers">
                <list>
                        <bean class="path.to.APanelConverter" />
                </list>
        </property>
</bean>

Armazenando as informações de um painel

Através de uma implementação da interface ICockpitPanelConfigurationDAO (jmine.tec.ux.api.cockpit.dao.ICockpitPanelConfigurationDAO) é possível persistir as configurações dos painéis criados, sendo possível persistir, inclusive, a sessão atual do usuário.

Ainda é possível indicar quais configurações serão salvas ou removidas através da classe CockpitPersister(jmine.tec.web.wicket.cockpit.persister.CockpitSessionPersister), utilizando-se os métodos CockpitPersister#save(ICockpitPanelConfiguration entity) e CockpitPersister#remove(ICockpitPanelConfiguration entity) respectivamente.

Utilizando-se os métodos supracitados, as configurações são passadas para a lista de configurações, de forma que outros métodos auxiliares do DAO possam selecionar, posteriormente:

  • Os painéis maximizados, através do método ICockpitPanelConfigurationDAO#findMaximized();
  • Os painéis fixados, através do método ICockpitPanelConfigurationDAO#findAttached();
  • Os painéis soltos, através do método ICockpitPanelConfigurationDAO#findDetached().

Os métodos citados anteriormente retornam a lista de configurações dos painéis (no caso do painel maximizado, apenas a configuração deste), para serem repassados ao método CockpitPanelContainer#loadPanelConfigurations() (citado anteriormente, responsável por retornar todas as configurações que serão utilizadas para construir os painéis dentro do container).

Criando um painel

Para criar um painel, primeiramente é necessária a criação de uma classe para esse painel, que é uma extensão da classe Panel (org.apache.wicket.markup.html.panel.Panel).

Exemplo de implementação:

public class CockpitPanelTeste extends Panel {
        public CockpitPanelTeste(String id) {
                super(id);
        }
}

Para a exibição correta do painel no Cockpit, é necessário criar uma classe Converter, de forma que seja possível à página do Cockpit acessar os métodos necessários para a conversão do painel em parâmetros e vice-versa. Tal classe deve implementar a classe CockpitPanelConverter(jmine.tec.web.wicket.cockpit.component.panel.ICockpitPanelConverter).

Exemplo de implementação:

public class CockpitPanelTesteConverter implements
CockpitPanelConverter<CockpitPanelTeste> {

        public static final String KEY = "NADA";

        public CockpitPanelTeste convertToPanel(String id,
                        CockpitPanelParameters parameters) {
                return new CockpitPanelTeste(id);
        }

        public CockpitPanelParameters convertToParameters(CockpitPanelTeste panel) {
                return parameters().thatContains(
                                aParameter().withKey(KEY).withValue(null)).create();
        }

        public Class<CockpitPanelTeste> getTargetClass() {
                return CockpitPanelTeste.class;
        }

}

A seguir, é necessário registrar o Converter através de um bean no arquivo xml do Cockpit, de forma que seja possível a conversão dos parâmetros a serem configurados na sequência. A seguir, um bean seguindo os exemplos anteriores.

Exemplo:

<bean id="cockpitPanelConverters" class="jmine.tec.utils.register.Registrar">
        <property name="receiver" ref="cockpitPanelRegistry" />
        <property name="registers">
                <list>
                        <bean class="jmine.tec.web.wicket.cockpit.component.panel.form.CockpitPanelTesteConverter" />
                </list>
        </property>
</bean>

O próximo passo é criar um arquivo html, de mesmo nome da classe do painel. Esse html será utilizado para exibir o conteúdo do painel. Abaixo segue um exemplo de uma implementação básica.

Exemplo:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
        xmlns:wicket="http://wicket.apache.org/" xml:lang="en" lang="en">

<head>
</head>
<body>
        <wicket:panel> <!--Necessário para a criação do painel-->
                Sua implementação aqui :)
        </wicket:panel>
</body>
</html>

A partir deste ponto, o seu painel já está criado, mas ainda não está exibido. Para que o mesmo seja exibido na página do Cockpit, é necessário que algum componente do Cockpit faça uma requisição de criação do novo painel, tal como um item do menu do Cockpit ou botão de outro painel. No exemplo a seguir, nosso painel criado será criado através da adição de um evento de comportamento do Ajax em um destes elementos(objeto da classe AjaxEventBehavior (org.apache.wicket.ajax.AjaxEventBehavior)).

Exemplo de implementação:

botaoExemplo.add(new AjaxEventBehavior("onclick") { //Ao evento do clique do mouse
        @Override
        protected void onEvent(AjaxRequestTarget target) {
                CockpitPanelConfiguration configuration = aPanel() //um painel...
                                .ofType(CockpitPanelTeste.class) //...do Tipo CockpitPanelTeste
                                .withName("Teste") //... com nome "Teste" (escrito na aba do painel)
                                .thatIsAttached() //... que é fixado (pode ser solto ou maximizado também)
                                .create();
                cockpitPanelConfigurationPersister.save(configuration); //armazena a configuração do Cockpit

                botaoExemplo.send(botaoExemplo.getPage(), //envia o comando para a página do Cockpit
                                Broadcast.BREADTH, //espalha o comando para quem for receber um comando do tipo especificado à seguir (Payload)
                                new RefreshPanelContainerPayload(target,AbstractAttachedCockpitPanelContainer.KEY));
        }
});

Abaixo, ou exemplo, desta vez para um item de menu do Cockpit:

Exemplo de implementação:

//...
new CockpitMenuItem() {

        public void onClick(Component clickedComponent, AjaxRequestTarget target) {
                CockpitPanelConfiguration configuration =
                                aPanel() //um painel...
                                .ofType(CockpitPanelTeste.class) //...do tipo (nossa classe Teste)...
                                .withName("Nome exibido") //...com o nome...
                                .thatIsAttached() //...que é fixado.
                                .create();
                cockpitPanelConfigurationPersister.save(configuration); //salva as configurações do Cockpit
                clickedComponent.send(clickedComponent.getPage()
                                , Broadcast.BREADTH,
                                new RefreshPanelContainerPayload(target,
                                                AbstractAttachedCockpitPanelContainer.KEY));
        }
/...

Observe que houve a configuração do painel, através da classe CockpitPanelConfiguration (CockpitPanelConfiguration), que configura o tipo do seu painel, o nome do painel (que aparece na aba superior do painel criado) e o tipo do painel (se fixo, solto ou maximizado).

A criação do painel é chamada pela função estática aPanel, da classe CockpitPanelConfigurationBuilder (CockpitPanelConfigurationBuilder)

A partir de então, ao clicar no botão/elemento correspondente, o novo painel será exibido conforme a página configurada.

Atualização dos Componentes

A interação entre dois componentes é algo desejável quando a intenção é criar-se uma determinada funcionalidade que modifique de alguma forma o comportamento ou o conteúdo de outro componente, como um painel. Desta forma, é necessário haver a intercomunicação entre os componentes do Cockpit.

Todo componente pertencente ao Cockpit segue a hierarquia dos componentes Wicket, conforme mencionado anteriormente. Desta forma, são implementações da classe abstrata Component (org.apache.wicket.Component), que possui um método importante no que tange a interação entre componentes: o método Component#send(). Este método recebe um objeto da classe IEventSink (org.apache.wicket.event.IEventSink), interface que todo componente implementa, um objeto da classe Broadcast (org.apache.wicket.event.Broadcast) e um outro objeto, chamado de Payload.

O método send() envia o evento requerido para um objeto, apelidado de sink (ralo, em tradução livre), que será responsável por receber este evento. No caso do Cockpit, a página a qual pertence o componente é o componente em mais alto nível no que diz respeito a configuração do Cockpit. Desta forma, usando o método this.getPage(), podemos passar a página em questão como nosso sink.

Se o objeto que receber o evento (no caso, a página) eventualmente precisar espalhar/anunciar este evento para outros componentes que esperam por esse evento, entra em cena o objeto da classe Broadcast.

Essa classe é um tipo Enum, e possui 4 atributos:

  • BREADTH: define que o evento poderá ser espalhado como em uma busca em largura entre os componentes-filhos.
  • DEPTH: define que o evento poderá ser espalhado em profundidade entre os componentes-filhos.
  • BUBBLE: o espalhamento do evento aos componentes-filhos será feita por um algoritmo semelhante ao bubblesort.
  • EXACT: apenas a sink especificada receberá o evento. Neste caso, a página receberia o evento.

Desta forma, é possível definir de forma eficiente como o evento percorrerá a hierarquia de componentes. Para que o componente receptor possa utilizar o evento, é necessário envelopar o evento em uma classe Payload.

O Payload é uma classe auxiliar que serve como um envelope, contendo informações importante para o componente receptor do evento. Uma das informações contidas no Payload é uma referência a um objeto AjaxRequestTarget (org.apache.wicket.ajax.AjaxRequestTarget), que vamos chamar de target, bem como os métodos setter e getter. Este objeto produz envelopes usados no lado do cliente para atualizar os componentes, e possui como método de interesse AjaxRequestTarget#add(), que basicamente recebe um componente a ser adicionado na lista de componentes a serem renderizados e, consequentemente, atualizados quando a resposta ajax for recebida.

Um exemplo de implementação desta classe encontra-se abaixo.

Exemplo de implementação:

public class CockpitPanelPayloadTeste implements Serializable{

        private final AjaxRequestTarget target;

        public CockpitPanelPayloadTeste (AjaxRequestTarget target){
                this.target = target;
        }

        public AjaxRequestTarget getTarget(){
                return this.target;
        }
}

Desta forma, podemos enviar através do método send() um objeto do qual seja possível recuperar o target, possibilitando ao componente receptor adicionar-se à lista de componentes a serem renderizados ou adicionar outros componentes.

Ainda, para que o evento seja recebido, é necessário sobrescrever o método Component#onEvent(). Outra função da classe Payload fica explícita aqui; ao sobrescrever este método, é possível recuperar apenas os eventos que estejam envelopados em objetos Payload de interesse do componente, e até mesmo mais de um tipo de evento com comportamentos diferentes, deixando os eventos não relacionados para serem tratados por outros componentes. Veja o exemplo abaixo.

Exemplo de implementação:

//... código
@Override
        public void onEvent(IEvent<?> event) {
                Object payload = event.getPayload();
                if (payload instanceof CockpitPanelPayloadTeste) { //verifica se o evento recebido é o esperado.
                        AjaxRequestTarget target = ((CockpitPanelPayloadTeste) payload).getTarget();
                        target.add(this); //coloca esse painel para ser renderizado, o que equivale a atualizá-lo.
                }else {
                super.onEvent(event);
                }
        }
//...código
../../_images/Diagrama_Simplificado.png