Montando um guestbook com Seaside + Magma

127628430_6b665843a1_m.jpgA pedidos (na verdade, era para estar no singular), :P trago neste post informações sobre o Seaside e o Magma, mais especificamente sobre a integração entre eles com o objetivo de se criar aplicações que necessitam persistir objetos.

O Seaside dispensa apresentações. Já o Magma é um banco de dados multi-usuário orientado a objetos escrito totalmente em Smalltalk. Ele fornece alguns recursos como controle de concorrência via optimistic locking, protocolo simples para controle transacional, querying de objetos e tolerância a falhas.

Bom, pra falar a verdade, eu nem sei se tudo isso é verdade, mas é o que consta na página oficial do projeto. Se você tiver mais experiência sobre o Magma, sinta-se a vontade para comentar e/ou corrigir quaisquer erros que eu possa ter cometido.

Enfim, para que possamos aprender como integrar o Seaside e o Magma, criaremos uma aplicação simples de livro de visitas (guestbook), cujas mensagens enviadas pelos usuários serão persistidas em um repositório de objetos controlado pelo Magma.

Preparando a imagem

Supondo que você utiliza o Squeak e possui o Seaside devidamente instalado e configurado, o procedimento a seguir não é tão complicado. Primeiramente, abra o SqueakMap, clicando em World - Open - SqueakMap Package Loader. Em seguida, atualize a lista de pacotes selecionando a opção upgrade map from net a partir do menu de contexto. Finalmente, baixe os pacotes Magma: Magma client, Magma server e Magma tester. A versão que usei aqui foi a 1.0r39.

Para integrar o Seaside com o Magma, precisamos também instalar um pacote chamado Magma seaside (que, por sinal, não está disponível para download através do SqueakMap). Para baixar, abra o Monticello, clicando em World - Open - Monticello Browser. Clique no botão + Repository e adicione um repositório HTTP:

  1.  
  2. MCHttpRepository
  3.     location: ‘http://www.squeaksource.com/Magma’
  4.     user:
  5.     password:
  6.  

Ok… selecione o repositório recém-criado e pressione o botão Open para que seja exibida a lista de pacotes do repositório. Na lista que aparecer, selecione o pacote Magma seaside, selecionando também a última versão disponível (aqui eu usei a versão 4) na lista da direita. Pressione o botão Merge para baixar e incluir o pacote na sua imagem. Por fim, salve a imagem para manter as modificações.

Criando o repositório Magma

Agora que temos a imagem pronta, podemos aproveitar para criar o repositório (ou banco de dados), que será usado pela nossa aplicação para armazenar e recuperar objetos.

Abra uma instância do Workspace e execute o código abaixo:

  1.  
  2. MagmaRepositoryController create: ‘guestbook’ root: Dictionary new.
  3.  

Aguarde alguns segundos para que o Magma crie o repositório. Este comando que acabamos de executar cria uma pasta guestbook no diretório onde sua imagem está situada, com alguns arquivos dentro (applied.images, commitPackages, objects e objects.idx). Como eu não sei para que eles servem, então não vou tentar explicar aqui! :D

O que deve ser compreendido é que um banco orientado a objetos é diferente de um banco relacional. Não há tabelas, onde seus clientes, pedidos e chamados são agrupados. O que existe são apenas… objetos!

Neste exemplo, definimos um objeto-raíz do tipo Dictionary. Este objeto serve para que possamos, através dele, chegar aos outros objetos (nossos clientes, pedidos e chamados, por exemplo). Qualquer objeto pode ser utilizado como objeto-raíz: você pode criar uma classe MyRepository (contendo métodos e atributos que refletem suas estruturas de dados) e setar um objeto desta classe como objeto-raíz. No entanto, a utilização de um objeto Dictionary (que basicamente é uma coleção de pares do tipo chave-valor) é recomendada por facilitar a organização dos objetos em sub-grupos.

Criando uma aplicação Seaside integrada ao repositório Magma

Inicie o servidor HTTP:

  1.  
  2. WAKom startOn: 9090.
  3.  

Abra o utilitário de configuração e informe o usuário e senha, se necessário. Adicione um novo Entry point chamado guestbook. Configure da seguinte forma:

screenshot-seaside-mozilla-firefox.png screenshot-seaside-mozilla-firefox-1.png

O pacote Magma seaside fornece algumas classes para facilitar a integração de uma aplicação Seaside com um repositório Magma. O que precisamos fazer foi criar a aplicação, tendo como classe-base de configuração a classe WAMagmaConfiguration, fornecida pelo referido pacote. Ao utilizar esta classe, a classe WASession é automaticamente substituída por WAMagmaSession, que é uma especialização de WASession capaz de controlar a sessão Magma.

Apenas para constar, a integração do Seaside do Magma pode ocorrer de duas formas: através de um repositório local ou remoto. A opção entre uma forma ou outra depende basicamente da carga de usuários que a aplicação deverá suportar. Mais detalhes sobre isso podem ser conferidos na página do projeto Magma.

Salve as configurações.

O que será criado?

Veja algumas screenshots da aplicação rodando… é assim que ela ficará após concluída:

screenshot-seaside-mozilla-firefox-2.png screenshot-seaside-mozilla-firefox-3.png
screenshot-seaside-mozilla-firefox-4.png screenshot-seaside-mozilla-firefox-5.png

Desenvolvendo a aplicação

A nossa mensagem

Primeiramente, veja a classe GuestbookMessage (que serão as mensagens do nosso livro de visitas). Trata-se de um POSO (existe este termo?!) :D :

  1.  
  2. Object subclass: #GuestbookMessage
  3.     instanceVariableNames: ‘date fromName email url message’
  4.     classVariableNames:
  5.     poolDictionaries:
  6.     category: ‘Guestbook-App’
  7.  
  8. "Accessor methods and initializer"
  9.  

Definindo o componente-raíz

Precisamos criar o componente-raíz da aplicação web. Veja:

  1.  
  2. WAComponent subclass: #GuestbookView
  3.     instanceVariableNames: ‘listComponent’
  4.     classVariableNames:
  5.     poolDictionaries:
  6.     category: ‘Guestbook-App’
  7.  
  8. "Accessor methods"
  9.  
  10. GuestbookView>>children
  11.     ^Array with: self listComponent
  12.  
  13. GuestbookView>>renderContentOn: html
  14.    self listComponent messages: self loadMessages.
  15.    html heading: ‘Guestbook application’.
  16.    html paragraph
  17.       with: [html render: self listComponent].
  18.    html paragraph
  19.       with: [html text: ‘* batteries not included, 2007. All rights NOT reserved! :-)’]
  20.  
  21. GuestbookView>>loadMessages
  22.    | data |
  23.    self session
  24.       commit: [data := self session root at: ‘messages’ ifAbsentPut: OrderedCollection new].
  25.    ^ data
  26.  

Dando uma olhada no método #renderContentOn:, podemos considerar este componente como sendo um template, pois ele servirá para renderizar um cabeçalho e um rodapé em todas as telas da aplicação. Isso é possível através do uso dos métodos #call: e #answer: ao longo da implementação, fazendo com que a “casca” da página seja renderizada sempre, enquanto o corpo da página a ser renderizada dependa do componente no controle. Mais detalhes sobre esse comportamento podem ser vistos em um post anterior aqui do blog.

Voltando ao assunto, veja que esta classe possui uma variável de instância listComponent, que será utilizada para guardar uma referência ao componente responsável por exibir a lista de mensagens cadastradas. O método #renderContentOn: solicita a renderização deste componente através de uma chamada ao método #render:.

O método #loadMessages também é interessante, pois é através deste método que recuperamos as mensagens cadastradas do repositório.

Toda operação a ser feita em um repositório Magma deve ser feita dentro de uma transação:

  1.  
  2. | mySession |
  3. mySession := "get Magma session object".
  4. mySession commit: [ "put database operations here" ].
  5. "modifications are already persistent at this point"
  6.  

No caso da nossa aplicação Seaside, a sessão Magma pode ser obtida através de uma chamada ao método self session:

  1.  
  2. "somewhere inside a Seaside component…"
  3. | mySession rootObj |
  4. mySession := self session. "WAMagmaSession object"
  5. rootObj := mySession root. "this is the root object"
  6.  

Ainda sobre o método #loadMessages…. você se lembra de que foi dito que é recomendado utilizar um Dictionary como objeto-raíz do repositório para facilitar a organização do mesmo em sub-grupos? Então… neste nosso aplicativo, todas as mensagens cadastradas são agrupadas sob a chave 'messages', em um objeto OrderedCollection, que contém as mensagens na ordem em que elas são inseridas.

Criando um componente para listar as mensagens

Veja, abaixo, o código principal do componente MessageList, que servirá para renderizar uma tabela HTML com as mensagens cadastradas (além de um link que leva à página de cadastro de mensagens):

  1.  
  2. WAComponent subclass: #MessageList
  3.     instanceVariableNames: ‘messages’
  4.     classVariableNames:
  5.     poolDictionaries:
  6.     category: ‘Guestbook-App’
  7.  
  8. "Accessor methods and initializer"
  9.  
  10. MessageList>>removeSelected: aMessage
  11.    self
  12.       isolate: [(self confirm: ‘Do you really want to remove that message?’)
  13.             ifTrue: [self session
  14.                   commit: [(self session root at: ‘messages’)
  15.                         remove: aMessage
  16.                         ifAbsent: []]]]
  17.  
  18. MessageList>>renderContentOn: html
  19.    html
  20.       paragraph: [self messages
  21.             ifEmpty: [html heading: ‘No messages yet.’ level: 4]
  22.             ifNotEmpty: [html table border: 1;
  23.                   with: [html
  24.                         tableRow: [html
  25.                               tableHeading: [html text: ‘Date’].
  26.                            html
  27.                               tableHeading: [html text: ‘Name’].
  28.                            html
  29.                               tableHeading: [html text: ‘URL’].
  30.                            html
  31.                               tableHeading: [html text: ‘Message’].
  32.                            html
  33.                               tableHeading: []].
  34.                      self messages
  35.                         reverseDo: [:each | html
  36.                               tableRow: [html
  37.                                     tableData: [html text: each date].
  38.                                  html
  39.                                     tableData: [html anchor url: ‘mailto:’ , each email;
  40.                                            with: each fromName].
  41.                                  html
  42.                                     tableData: [html anchor url: each url;
  43.                                            with: each url].
  44.                                  html
  45.                                     tableData: [html text: each message].
  46.                                  html
  47.                                     tableData: [html anchor
  48.                                           callback: [self removeSelected: each];
  49.                                            with: ‘X’]]]]]].
  50.    html
  51.       paragraph: [html anchor
  52.             callback: [self
  53.                   isolate: [self call: MessageForm new]];
  54.              with: ‘Add a new message’]
  55.  

Este componente é exibido assim que o usuário acessa a aplicação, iterando em ordem reversa pela lista de mensagens cadastradas, mostrando-as em uma tabela. Se ele deseja então cadastrar uma nova mensagem, ele clica no link (que faz com o componente MessageForm assuma o controle, sendo renderizado no lugar do componente que o chamou). Enfim, nada de novo aqui.

Destaque para o método #removeSelected:, que obtém a mensagem correspondente e a remove do repositório (após pedir uma confirmação).

Criando um componente para incluir uma mensagem

Abaixo, segue o código do componente MessageForm, utilizado para receber os dados do usuário através de um formulário e cadastrar a mensagem no repositório. Não há validação, nem nada disso… só estamos interessados em ver como o Magma se integra ao Seaside, tá lembrado? :D

  1.  
  2. WAComponent subclass: #MessageForm
  3.    instanceVariableNames: ‘message’
  4.    classVariableNames:
  5.    poolDictionaries:
  6.    category: ‘Guestbook-App’
  7.  
  8. "Accessor methods and initializer"
  9.  
  10. MessageForm>>insertMessage
  11.    self session
  12.       commit: [(self session root at: ‘messages’)
  13.             add: self message].
  14.    self answer
  15.  
  16. MessageForm>>renderContentOn: html
  17.    html
  18.       paragraph: [html
  19.             form: [html
  20.                   table: [html
  21.                         tableRow: [html
  22.                               tableData: [html text: ‘Name:’].
  23.                            html
  24.                               tableData: [html textInput on: #fromName of: self message]].
  25.                      html
  26.                         tableRow: [html
  27.                               tableData: [html text: ‘Email:’].
  28.                            html
  29.                               tableData: [html textInput on: #email of: self message]].
  30.                      html
  31.                         tableRow: [html
  32.                               tableData: [html text: ‘URL:’].
  33.                            html
  34.                               tableData: [html textInput on: #url of: self message]].
  35.                      html
  36.                         tableRow: [html
  37.                               tableData: [html text: ‘Message:’].
  38.                            html
  39.                               tableData: [html textArea rows: 6;
  40.                                      columns: 50;
  41.                                      on: #message of: self message]].
  42.                      html
  43.                         tableRow: [html tableData colSpan: 2;
  44.                                style: ‘text-align: right’;
  45.                              
  46.                               with: [html submitButton
  47.                                     callback: [self insertMessage];
  48.                                      value: ‘Send!’]]]]].
  49.    html
  50.       paragraph: [html anchor
  51.             callback: [self answer];
  52.              with: ‘Go back’]
  53.  

Nada de novo. Se o usuário decide voltar, ele pressiona o link Go back, fazendo com que o componente devolva o controle ao chamador (que, no caso, é o componente MessageList). Se o usuário preenche o formulário e o submete, o objeto GuestbookMessage é preenchido com os dados do formulário e o método #insertMessage é invocado.

O método #insertMessage, por sua vez, serve para pegar a mensagem digitada pelo usuário e adicionar ao repositório, devolvendo o controle ao componente MessageList em seguida.

Conclusão

É isso mesmo… a aplicação está pronta! Em poucos minutos, criamos uma aplicação web de livro de visitas com o Seaside, com suporte à persistência de objetos via repositório Magma. Uma vez configurada a aplicação, a integração entre os frameworks ocorre de forma praticamente transparente.

Só para dar uma instigada… esta aplicação poderia ser ter sido codificada ainda mais rapidamente, usando o Magritte. Com o Magritte, nós criaríamos métodos a nível de classe que especificariam meta-dados para a classe GuestbookMessage (um workaround para se conseguir algo como as annotations do Java, eu suponho), sendo possível gerar o código de formulários HTML dinamicamente (inclusive com suporte a validação) com apenas um comando!

Ficou curioso? Aguarde os próximos posts!

Foto por: OllieD

Tags: , , , , , , , , , , ,