Nível: Introdutório Scott Davis, Senior IT Architect, IBM, Software Group
09/Jun/2009 Nesta parte de Mastering
Grails, Scott Davis mostra como fazer o upload de arquivos em seu aplicativo Grails e configurar um Atom
Syndication feed. Feito isso, o Blogito se tornará um servidor de blogs totalmente desenvolvido.
Nos artigos anteriores de Mastering
Grails, você construiu um pequeno serviço de blog (Blogito) parte por parte. Neste artigo, o
Blogito finalmente seguirá seu destino e se tornará um aplicativo de blog funcional. Você vai implementar recursos de upload de arquivos
para o corpo de uma entrada de blog e construirá um Atom feed manualmente para organização.
 |
Sobre esta Série
Grails é uma estrutura de desenvolvimento de Web moderna que combina tecnologias Java familiares, como Spring e Hibernate,
com práticas contemporâneas, como convenção através de configuração. Gravado em Groovy, o Grails oferece integração direta com seu
código Java legado, enquanto inclui a flexibilidade e o dinamismo de uma linguagem de script. Depois de conhecer o Grails, você nunca mais
verá o desenvolvimento de Web com os mesmos olhos.
|
|
Mas antes de começar, a autenticação que incluí no artigo anterior ("Autenticação
e Autorização") introduziu um pequeno erro na UI. Essa correção deve ter precedência sobre a inclusão de novos recursos.
Corrigindo um Erro Oculto
Conforme você inicia o Grails, grails-app/conf/Bootstrap.groovy inclui dois usuários e quatro novas entradas de blog. Mas o que acontece
quando você tenta incluir uma entrada de blog através da interface da Web? Utilize as seguintes etapas para descobrir:
- Efetue login como
jsmith, digite a senha wordpass.
- Clique em Nova Entrada.
- Inclua um título e um resumo.
- Clique em Criar.
Oops! Você recebeu o seguinte erro: Property [author] of class [class Entry] cannot be null. Como
esse erro foi parar no aplicativo, principalmente com o código de autoinicialização trabalhando corretamente?
Eu fiz você gerar visualizações de Groovy Server Pages (GSP) no primeiro artigo do Blogito ("Renove
Seus Aplicativos Grails") digitando grails generate-views Entry. Nos artigos subsequentes, fiz mudanças na
classe de domínio, mas você não precisou voltar e gerar visualizações novamente. A visualização create.gsp no disco ficou ultrapassada
quando incluí o relacionamento 1:M entre Entry e User, conforme visto na
Lista 1. (Lembre-se de que belongsTo cria um campo chamado author que é do
tipo User.)
Lista 1. O Relacionamento 1:M que Quebrou a GSP
class Entry {
static belongsTo = [author:User]
String title
String summary
Date dateCreated
Date lastUpdated
}
|
Eu sei que é terrível lembrar que a armação dinâmica de suas visualizações — principalmente em estágios iniciais de
desenvolvimento quando o modelo de domínio está mudando rapidamente — é a maneira mais segura de manter tudo em
sincronia. Certamente, você não pode contar apenas com visualizações armadas, mas no momento em que você gera GSPs no
disco, a responsabilidade de mantê-las atualizadas passa do Grails para você.
Se você gerou as visualizações agora para a classe Entry, o Grails irá fornecer uma caixa de
combinação que exibe uma lista de Authors, conforme mostrado na Lista 2. Não faça
isso sozinho — isso serve apenas para ilustrar um ponto. Daqui a pouco vou dar a você algumas opções diferentes.
Lista 2. Caixa de Combinação Gerada para Relacionamentos 1:M
<g:form action="save" method="post" >
<div class="dialog">
<table>
<tbody>
<!-- SNIP -->
<tr class="prop">
<td valign="top" class="name">
<label for="author">Author:</label>
</td>
<td valign="top"
class="value ${hasErrors(bean:entryInstance,
field:'author','errors')}">
<g:select optionKey="id"
from="${User.list()}"
name="author.id"
value="${entryInstance?.author?.id}" ></g:select>
</td>
</tr>
<!-- SNIP -->
</tbody>
</table>
</div>
</g:form>
|
Observe o elemento <g:select>. O nome do campo é author.id. Como
você aprendeu em "GORM: Nome Engraçado, Tecnologia Séria,"
o texto exibido na lista vem do método User.toString(). Normalmente, isso é o que seria enviado
de volta como valor do campo para o servidor quando o formulário fosse enviado. Nesse caso, o atributo
optionKey substitui o valor do campo, enviando de volta o id do
Autor em seu lugar. (Para obter mais informações sobre a tag <g:select>, consulte
Recursos.)
A maneira mais rápida de fornecer ao EntryController.groovy o campo author.id que ele está
procurando é incluir um campo oculto no formulário, conforme mostrado na Lista 3. Como você deve ter efetuado login
para acessar a ação create, e o User é o author da
entrada de blog, você pode utilizar com segurança session.user.id para o valor.
Lista 3. Passando o Campo author.id do Formulário
<g:form action="save" method="post" >
<input type="hidden" name="author.id" value="${session.user.id}" />
<!-- SNIP -->
</g:form>
|
Para um aplicativo simples como o Blogito, provavelmente isso é suficiente. Porém, existe a possibilidade de um hacker do
lado do cliente estar inserindo um valor diferente para author.id. Para estar completamente
seguro, você pode incluir Entry.author no encerramento save, como
mostra a Lista 4:
Lista 4. Salvando author.id no Servidor
def save = {
def entryInstance = new Entry(params)
entryInstance.author = User.get(session.user.id)
if(!entryInstance.hasErrors() && entryInstance.save()) {
flash.message = "Entry ${entryInstance.id} created"
redirect(action:show,id:entryInstance.id)
}
else {
render(view:'create',model:[entryInstance:entryInstance])
}
}
|
Esse é o padrão de encerramento save que você obtém quando gera o controlador, mais uma linha
de código customizado. A linha entryInstance.author obtém o User do
banco de dados com base no valor session.user.id e preenche o campo
Entry.author.
Na próxima seção, você vai customizar o encerramento save para fazer uploads de arquivos,
portanto, você também pode errar na questão da segurança e incluir o código na Lista 4 em
EntryController.groovy. Reinicie o Grails e certifique-se de poder incluir com sucesso uma nova Entry através
do formulário HTML.
Upload de Arquivo
Agora que a criação de Entry está funcionando novamente, é hora de incluir outro recurso. Quero
que os usuários estejam preparados para fazer o upload de um arquivo quando criarem uma nova Entry. O
arquivo poderia ser um HTML contendo o corpo inteiro da entrada de blog, ou uma imagem, ou qualquer outra coisa. Para isso, você precisa
acessar a classe de domínio Entry, EntryController e visualizações
de GSP — e incluir uma nova TagLib nessa combinação.
Para começar, dê uma olhada em grails-app/views/entry/create.gsp. Inclua um novo campo para fazer o upload do arquivo,
conforme mostrado na Lista 5:
Lista 5. Incluindo um Campo de Upload de Arquivo
<g:uploadForm action="save" method="post" >
<!-- SNIP -->
<tr class="prop">
<td valign="top" class="name">
<label for="payload">File:</label>
</td>
<td valign="top">
<input type="file" id="payload" name="payload"/>
</td>
</tr>
</g:uploadForm>
|
Observe que as tags <g:form> foram alteradas para <g:uploadForm>. Isso
permite uploads de arquivos a partir do formulário HTML. Você também poderia ter deixado as tags <g:form>
no lugar e ter apenas incluído um atributo enctype="multipart/form-data". (O padrão enctype for HTML forms is application/x-www-form-urlencoded.)
Após o formulário ter seu enctype configurado corretamente (ou se você utilizar
<g:uploadForm>), é possível incluir o campo <input type="file" />. Isso fornece
ao usuário um botão para navegar no sistema de arquivos local e selecionar um arquivo para ser transferido por upload, conforme
mostrado na Figura 1. Meus exemplos utilizam o logotipo do Grails; você pode utilizar qualquer arquivo de imagem que quiser.
Figura 1. O Formulário Criar Entrada com um Campo de Upload de Arquivo
Agora que o formulário do lado do cliente está pronto, é hora de ajustar o código do lado do servidor para algo útil com
o arquivo transferido por upload. Abra grails-app/controllers/EntryController.groovy em um editor de texto e inclua o
código na Lista 6 para o encerramento save:
Lista 6. Mostrando Informações sobre o Arquivo Transferido por Upload
def save = {
def entryInstance = new Entry(params)
entryInstance.author = User.get(session.user.id)
//tratar arquivo transferido por upload
def uploadedFile = request.getFile('payload')
if(!uploadedFile.empty){
println "Class: ${uploadedFile.class}"
println "Name: ${uploadedFile.name}"
println "OriginalFileName: ${uploadedFile.originalFilename}"
println "Size: ${uploadedFile.size}"
println "ContentType: ${uploadedFile.contentType}"
}
if(!entryInstance.hasErrors() && entryInstance.save()) {
flash.message = "Entry ${entryInstance.id} created"
redirect(action:show,id:entryInstance.id)
}
else {
render(view:'create',model:[entryInstance:entryInstance])
}
}
|
Observe que você utiliza o método request.getFile() para obter uma referência ao arquivo
transferido por upload. Depois isso, você pode fazer todos os tipos de introspecção nele. A Lista 7 mostra a
saída do console após o logotipo do Grails ser transferido por upload:
Lista 7. Saída do Console a partir do Arquivo Transferido por Upload
Class: class org.springframework.web.multipart.commons.CommonsMultipartFile
Name: payload
OriginalFileName: Grails_logo.jpg
Size: 8065
ContentType: image/jpeg
|
Sabendo que o Grails utiliza a estrutura Spring MVC em segredo, não é nenhuma surpresa saber que o arquivo transferido por
upload é disponibilizado para o controlador como um objeto CommonsMultipartFile. Além de expor
o nome do campo de formulário HTML, essa classe também fornece a você acesso ao nome do arquivo original, ao tamanho em
bytes e ao tipo MIME do arquivo.
A próxima etapa é salvar o arquivo transferido por upload em algum lugar. Inclua mais algumas linhas de código no
encerramento save, conforme mostrado na Lista 8:
Lista 8. Salvando o Arquivo Transferido por Upload em Disco
def save = {
def entryInstance = new Entry(params)
entryInstance.author = User.get(session.user.id)
//tratar arquivo transferido por upload
def uploadedFile = request.getFile('payload')
if(!uploadedFile.empty){
println "Class: ${uploadedFile.class}"
println "Name: ${uploadedFile.name}"
println "OriginalFileName: ${uploadedFile.originalFilename}"
println "Size: ${uploadedFile.size}"
println "ContentType: ${uploadedFile.contentType}"
def webRootDir = servletContext.getRealPath("/")
def userDir = new File(webRootDir, "/payload/${session.user.login}")
userDir.mkdirs()
uploadedFile.transferTo( new File( userDir, uploadedFile.originalFilename))
}
if(!entryInstance.hasErrors() && entryInstance.save()) {
flash.message = "Entry ${entryInstance.id} created"
redirect(action:show,id:entryInstance.id)
}
else {
render(view:'create',model:[entryInstance:entryInstance])
}
}
|
Após criar o diretório payload/jsmith sob a raiz da Web, você pode utilizar o método uploadedFile.transferTo()
para salvar o arquivo no disco. O método File.mkdirs() é não destrutivo, portanto, você pode
chamá-lo repetidamente sem se preocupar com a perda de arquivos existentes, caso o diretório já exista.
Em seguida, inclua um campo String na classe Entry para armazenar
o filename, conforme mostrado na Lista 9. Certifique-se de incluir uma restrição que permita
um novo campo em branco (no formulário HTML) e anulável (no banco de
dados).
Lista 9. Incluindo o Campo filename em Entry
class Entry {
static constraints = {
title()
summary(maxSize:1000)
filename(blank:true, nullable:true)
dateCreated()
lastUpdated()
}
static mapping = {
sort "lastUpdated":"desc"
}
static belongsTo = [author:User]
String title
String summary
String filename
Date dateCreated
Date lastUpdated
}
|
Por fim, inclua filename no objeto Entry no encerramento
save. A Lista 10 mostra o encerramento save completo:
Lista 10. Armazenando filename em Entry
def save = {
def entryInstance = new Entry(params)
entryInstance.author = User.get(session.user.id)
//tratar arquivo transferido por upload
def uploadedFile = request.getFile('payload')
if(!uploadedFile.empty){
println "Class: ${uploadedFile.class}"
println "Name: ${uploadedFile.name}"
println "OriginalFileName: ${uploadedFile.originalFilename}"
println "Size: ${uploadedFile.size}"
println "ContentType: ${uploadedFile.contentType}"
def webRootDir = servletContext.getRealPath("/")
def userDir = new File(webRootDir, "/payload/${session.user.login}")
userDir.mkdirs()
uploadedFile.transferTo( new File( userDir, uploadedFile.originalFilename))
entryInstance.filename = uploadedFile.originalFilename
}
if(!entryInstance.hasErrors() && entryInstance.save()) {
flash.message = "Entry ${entryInstance.id} created"
redirect(action:show,id:entryInstance.id)
}
else {
render(view:'create',model:[entryInstance:entryInstance])
}
}
|
Uma estratégia alternativa de salvar arquivos transferidos por upload no sistema de arquivos é armazená-los diretamente
no banco de dados. Se você criar um campo byte[] denominado payload em
Entry, será possível ignorar completamente todos os códigos customizados incluídos no encerramento save. Mas,
se fizer isso, você vai perder toda a graça da próxima seção.
Exibindo o Arquivo Transferido por Upload
Qual é seu objetivo ao transferir um arquivo por upload e não exibi-lo em nenhum lugar? Abra grails-app/views/entry/_entry.gsp
e inclua o código na Lista 11:
Lista 11. Código de GSP para Exibir a Imagem Transferida por Upload
<div class="entry">
<span class="entry-date">
<g:longDate>${entryInstance.lastUpdated}</g:longDate> : ${entryInstance.author}
</span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}</g:link></h2>
<p>${entryInstance.summary}</p>
<g:if test="${entryInstance.filename}">
<p>
<img src="${createLinkTo(dir:'payload/'+entryInstance.author.login,
file:''+entryInstance.filename)}"
alt="${entryInstance.filename}"
title="${entryInstance.filename}" />
</p>
</g:if>
</div>
|
Como o upload do arquivo é opcional, eu dividi a saída em um bloco <g:if>. Se o
campo entryInstance.filename estiver preenchido, exibirei os resultados em uma tag
<img>.
A Figura 2 mostra a nova lista, exibindo orgulhosamente o logotipo do Grails transferido por upload:
Figura 2. Exibindo a Imagem Transferida por Upload
Mas e se o usuário fizer o upload de alguma coisa que não seja uma imagem? Em vez de colocar mais lógica na GSP, este parece
o lugar perfeito para uma TagLib customizada.
Criando uma TagLib
O Blogito já possui duas TagLibs em grails-app/taglib: DateTagLib.groovy e LoginTagLib.groovy. Você pode definir quantas
tags customizadas quiser em uma única TagLib, mas dessa vez, recomendo criar uma nova apenas para manter as tags
semanticamente agrupadas. Digite grails create-tag-lib Entry no prompt de comandos e inclua o
código na Lista 12:
Lista 12. Criando a Tag displayFile
class EntryTagLib {
def displayFile = {attrs, body->
def user = attrs["user"]
def filename = attrs["filename"]
if(filename){
def extension = filename.split("\\.")[-1]
def userDir = "payload/${user}"
switch(extension.toUpperCase()){
case ["JPG", "PNG", "GIF"]:
def html = """
<p>
<img src="${createLinkTo(dir:''+userDir,
file:''+filename)}"
alt="${filename}"
title="${filename}" />
</p>
"""
out << html
break
case "HTML":
out << "p>html</p>"
break
default:
out << "<p>file</p>"
break
}
}else{
out << "<!-- no file -->"
}
}
}
|
Como você verá em breve, esse código cria uma tag <g:displayFile> que espera
dois atributos: user e filename. Se o atributo
filename estiver preenchido, a extensão do arquivo será manipulada e convertida em maiúscula.
Switch , que é uma instrução no Groovy, oferece muito mais flexibilidade do que sua contraparte
Java. Para iniciantes, você pode alternar Strings. (A linguagem Java só pode alternar int.) Muito
mais impressionante é que case pode especificar uma List de condições, bem como uma única condição.
Com essa TagLib pronta, o modelo parcial _entry.gsp pode ser totalmente simplificado, conforme mostrado na Lista 13:
Lista 13. O Modelo Parcial Simplificado
<div class="entry">
<span class="entry-date">
<g:longDate>${entryInstance.lastUpdated}</g:longDate> : ${entryInstance.author}
</span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}</g:link></h2>
<p>${entryInstance.summary}</p>
<g:displayFile filename="${entryInstance.filename}"
user="${entryInstance.author.login}" />
</div>
|
Reinicie o Grails e faça o upload do seu logotipo mais uma vez. Antes de incluir suporte para os outros tipos de arquivos,
certifique-se de que a refatoração da TagLib não interrompeu a funcionalidade existente.
Agora que você tem certeza de que o upload de uma imagem funciona, a inclusão de suporte para outros tipos de arquivos é
uma questão de implementar cases apropriados no bloco switch. A Lista
14 demonstra como tratar um HTML transferido por upload, bem como simplesmente criar um link para o download de um arquivo para
o case padrão:
Lista 14. O Bloco switch/case Inteiro
class EntryTagLib {
def displayFile = {attrs, body->
def user = attrs["user"]
def filename = attrs["filename"]
if(filename){
def extension = filename.split("\\.")[-1]
def userDir = "payload/${user}"
switch(extension.toUpperCase()){
case ["JPG", "PNG", "GIF"]:
//SNIP
break
case "HTML":
def webRootDir = servletContext.getRealPath("/")
out << new File(webRootDir+"/"+userDir, filename).text
break
default:
def html = """
<p>
<a href="${createLinkTo(dir:''+userDir,
file:''+filename)}">${filename}</a>
</p>
"""
out << html
break
}
}else{
out << "<!-- no file -->"
}
}
}
|
Crie dois novos arquivos de texto para exercitar esse novo comportamento: um denominado test.html e o outro denominado
noextension. Inclua o conteúdo da Lista 15 no arquivo apropriado, faça seu upload e verifique se a TagLib exibe cada
um conforme esperado:
Lista 15. Dois Arquivos de Amostra para Upload
//test.html
<p>
Este é um <b>test</b> HTML.
</p>
<p>
Aqui está um link para a página inicial do <a href="http://grails.org">Grails</a>.
</p>
<p>
E aqui está um link para
<img src="http://grails.org/images/grails-logo.png">Grails Logo</img>.
</p>
//noextension
Este arquivo não tem uma extensão.
|
Seu navegador da Web deve ser semelhante à Figura 3:
Figura 3. Mostrando os Três Tipos de Arquivos Transferidos por Upload
Incluindo um Atom Feed
Neste ponto, você deverá ver uma formação padrão distinta. Para cada novo recurso incluído em seu aplicativo Grails, é
muito provável que você tenha contato com o modelo, a visualização e o controlador. Talvez também seja preciso incluir
um modelo parcial ou uma TagLib ao longo do caminho por medida de segurança.
A inclusão de um Atom feed no Blogito segue este padrão. Não é necessário alterar o modelo, mas você vai acabar fazendo
muito mais. Você vai:
- Incluir um encerramento no controlador
Entry para tratar do pedido Atom.
- Criar uma nova página de GSP para renderizar os resultados como um documento Atom bem formado.
- Criar um novo modelo parcial e uma nova tag customizada para acelerar as coisas.
Você poderia instalar um belo plug-in Feeds que inclua recursos RSS e Atom em seu aplicativo Grails (consulte Recursos),
mas eu suponho que você vai achar o formato Atom muito simples para se equipar sozinho. Para comprovar isso, você pode
visualizar a origem de um Atom feed existente ou verificar o exemplo no final da página Wikipedia no Atom (consulte
Recursos). Você poderia até ler RFC 4287, a especificação IETF para o formato Atom (consulte Recursos). Ou
pode simplesmente continuar lendo aqui para ver uma solução específica para o Grails.
Para começar, inclua um encerramento atom em EntryController.groovy, conforme mostrado na
Lista 16:
Lista 16. Adding an atom closure to EntryController.groovy
def atom = {
if(!params.max) params.max = 10
def list = Entry.list( params )
def lastUpdated = list[0].lastUpdated
[ entryInstanceList:list, lastUpdated:lastUpdated ]
}
|
A única diferença entre este e o encerramento padrão list é a inclusão do campo
lastUpdated. Como a lista já está classificada por lastUpdated (graças
à configuração sort "lastUpdated":"desc" no bloco static mapping
na classe de domínio Entry), a captura deste campo da primeira Entry na
lista lhe dá efetivamente a data mais recente.
Em seguida, crie grails-app/views/entry/atom.gsp. Inclua o código na Lista 17:
Lista 17. atom.gsp
<% response.setContentType("application/atom+xml")
%><feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">News from Blogito.org</title>
<link rel="alternate" type="text/html" href="http://blogito.org/"/>
<link rel="self" type="application/atom+xml" href="http://blogito.org/entry/atom" />
<updated><g:atomDate>${lastUpdated}</g:atomDate></updated>
<author><name>Blogito.org</name></author>
<id>tag:blogito.org,2009-01-01:entry/atom</id>
<generator uri="http://blogito.org" version="0.1">Hand-rolled Grails code</generator>
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<g:render template="atomEntry" bean="${entryInstance}" var="entryInstance" />
</g:each>
</feed>
|
Como você pode ver, a primeira coisa que você faz é configurar o tipo MIME como application/atom+xml. Depois
disso, você fornece alguns metadados básicos sobre o feed: updated, author, generator
, e assim por diante.
Se quiser evitar a codificação permanente de blogito.org no feed, você pode fazer o encerramento
atom capturar request.serverName, designá-lo a uma variável e retorná-lo
no hashmap de resposta com entryInstanceList e lastUpdated. Para ser
totalmente dinâmico, você pode utilizar request.scheme para retornar httpe request.serverPort
para retornar 80, também. (O único local onde você deve evitar o uso da variável request.serverName é no id, que será discutido em alguns instantes.)
É comum para um Atom feed fornecer links em vários formatos diferentes. Como você pode perceber com base no atributo
type, esse feed oferece dois: um link HTML e um de volta para si mesmo no formato Atom. O auto link
é especialmente útil; se você acabar com um documento Atom que você não transferiu por download, este é seu rastro de volta
para a origem canônica.
O campo id é o identificador exclusivo para o Atom feed, diferente do URI ou local atual onde
ele pode ser transferido por download. (Como você acabou de aprender, o elemento <link>
fornece a origem atual do feed.) Neste exemplo, eu utilizo a técnica descrita por Mark Pilgrim para gerar uma cadeia de ID
exclusiva permanente: combinar seu nome de domínio, a primeira data em que seu feed entrou em serviço e o restante do URI. (Consulte
Recursos para obter mais informações.)
As partes individuais do id não são tão importantes quanto à imparidade do todo. Certifique-se
de que esse id não mude com o tempo através da transmissão inadvertida de variáveis do controlar — no
caso do id do feed, você quer que ele seja exclusivo e invariável. Mesmo se o endereço do
seu servidor mudar, o id do feed deve permanecer inalterado se o conteúdo permanecer inalterado.
O campo atualizado precisa estar em um formato específico — 2003-12-13T18:30:02Z, ou RFC 3339 para ser
mais exato. (Consulte Recursos para obter detalhes.) Inclua um encerramento atomDate
no arquivo grails-app/taglib/DateTagLib.groovy existente, conforme mostrado na Lista 18:
Lista 18. Incluindo a tag atomDate
import java.text.SimpleDateFormat
class DateTagLib {
public static final String INCOMING_DATE_FORMAT = "yyyy-MM-dd hh:mm:ss"
public static final String ATOM_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'-07:00'"
def atomDate = {attrs, body ->
def b = attrs.body ?: body()
def d = new SimpleDateFormat(INCOMING_DATE_FORMAT).parse(b)
out << new SimpleDateFormat(ATOM_DATE_FORMAT).format(d)
}
//SNIP
}
|
Para concluir o Atom feed, crie grails-app/views/entry/_atomEntry.gsp e inclua o código na Lista 19:
Lista 19. O Modelo Parcial _atomEntry.gsp
<entry xmlns='http://www.w3.org/2005/Atom'>
<author>
<name>${entryInstance.author.name}</name>
</author>
<published><g:atomDate>${entryInstance.dateCreated}</g:atomDate></published>
<updated><g:atomDate>${entryInstance.lastUpdated}</g:atomDate></updated>
<link href="http://blogito.org/blog/${entryInstance.author.login}/
${entryInstance.title.encodeAsUnderscore()}" rel="alternate"
title="${entryInstance.title}" type="text/html" />
<id>tag:blogito.org,2009:/blog/${entryInstance.author.login}/
${entryInstance.title.encodeAsUnderscore()}</id>
<title type="text">${entryInstance.title}</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
${entryInstance.summary}
</div>
</content>
</entry>
|
A última coisa que você precisa fazer é liberar o Atom feed para usuários não autenticados. Ajuste beforeInterceptor em
EntryController.groovy, conforme mostrado na Lista 20:
Lista 20. Abrindo o Atom Feed para Usuários Não Autenticados
class EntryController {
def beforeInterceptor = [action:this.&auth, except:["index", "list", "show", "atom"]]
//SNIP
}
|
Reinicie o Grails; sua visita a http://localhost:9090/blogito/entry/atom deve gerar um Atom feed bem formado, conforme
mostrado na Lista 21:
Lista 21. O Atom Feed Bem Formado
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">News from Blogito.org</title>
<link rel="alternate" type="text/html" href="http://blogito.org/"/>
<link rel="self" type="application/atom+xml" href="http://blogito.org/entry/atom" />
<updated>2009-04-20T00:03:34-07:00</updated>
<author><name>Blogito.org</name></author>
<id>tag:blogito.org,2009-01-01:entry/atom</id>
<generator uri="http://blogito.org" version="0.1">Hand-rolled Grails code</generator>
<entry xmlns='http://www.w3.org/2005/Atom'>
<author>
<name>Jane Smith</name>
</author>
<published>2009-04-20T00:03:34-07:00</published>
<updated>2009-04-20T00:03:34-07:00</updated>
<link href="http://blogito.org/blog/jsmith/Testing_with_Groovy" rel="alternate"
title="Testing with Groovy" type="text/html" />
<id>tag:blogito.org,2009:/blog/jsmith/Testing_with_Groovy</id>
<title type="text">Testing with Groovy</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
See Practically Groovy
</div>
</content>
<!-- SNIP -->
</entry>
</feed>
|
Embora a semântica do Atom seja nova para você, a mecânica de produção de um Atom feed com o uso do Grails deve ser bastante
direta.
Validando o Atom Feed
Para verificar se o feed é, de fato, um Atom bem formado, visite o Feed Validator on-line da W3C (consulte Recursos). Se
seu feed estiver em um URI publicamente acessível, você poderá colá-lo na página inicial e clicar em Verificar. Seu Atom feed está
em execução em um host local, portanto, clique em Validar por Entrada Direta e cole a saída do feed. Os resultados
são mostrados na Figura 4:
Figura 4. O Validador W3C
Em vez de um aviso de que o autolink não está disponível no URI fornecido — que é obviamente o caso— seu
Atom feed deve ser considerado válido e pronto para produção.
Incluindo o Ícone do Feed
A etapa final é a inclusão de um link no feed para o cabeçalho. Você pode fazer o download do ícone do feed onipresente
a partir de vários lugares na Web; ele é liberado sob a licença do Mozilla de software livre (consulte Recursos).
Copie o arquivo em web-app/images e ajuste grails-app/views/layouts/_header.gsp conforme mostrado na Lista 22:
Lista 22. Incluindo o Ícone do Feed no Cabeçalho
<div id="header">
<p><g:link class="header-main" controller="entry">Blogito</g:link></p>
<p class="header-sub">
<g:link controller="entry" action="atom">
<img src="${createLinkTo(
dir:'images',file:'feed-icon-28x28.png')}" alt="Subscribe" title="Subscribe"/>
</g:link>
A tiny little blog
</p>
<div id="loginHeader">
<g:loginControl />
</div>
</div>
|
Os resultados devem ser uma página inicial semelhante à Figura 5:
Figura 5. Página Inicial do Blogito com o Ícone do Feed
Conclusão
Neste artigo, você incluiu recursos de upload de arquivo, bem como um Atom Syndication feed.
E agora veja o que você conseguiu: o Blogito é um pequeno servidor de blog funcional. Mas o quanto ele é pequeno? Duas classes
de domínio, dois controladores e um pouco mais de 250 linhas de código. Digite grails stats para
verificar isso. A Lista 23 mostra os resultados:
Lista 23. O Tamanho do Blogito
$ grails stats
+----------------------+----------+-------+
| Nome | Arquivos | LOC |
+----------------------+----------+-------+
| Controladores | 2 | 127 |
| Classes de Domínio | 2 | 34 |
| Bibliotecas de Tag | 3 | 66 |
| Testes de Unidade | 6 | 24 |
| Testes de Integração | 1 | 10 |
+----------------------+----------+-------+
| Totais | 14 | 261 |
+----------------------+----------+-------+
|
Embora esse exercício tenha abrangido quatro artigos, a realidade é que ele representa cerca de um único dia de esforços
de desenvolvimento após você ter um conhecimento de trabalho sólido do Grails.
Espero que você tenha gostado de juntar as peças do Blogito. Da próxima vez, você vai incluir suporte para comentários, tags e muito
mais através dos plug-ins correspondentes. E nas partes subsequentes, vou explorar o ecossistema do plug-in Grails com
você. Por enquanto, divirta-se dominando o Grails.
Recursos Aprender
Obter produtos e tecnologias
-
Grails: Faça o download do release mais recente do Grails.
-
Plug-in do Feeds: Um plug-in Grails que renderiza RSS/Atom feeds e podcasts
compatíveis com iTunes.
-
Ícone do Feed: Você pode selecionar o ícone do feed padrão no site Feed Icon ou
em Wikimedia Commons.
-
Blogito: É possível fazer o download do aplicativo Blogito completo.
Discutir
Sobre o autor
Avalie esta página
|