清单 13. 简化的局部模板
<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> |
重新启动 Grails,并再次上传 Grails 徽标。在添加对其他文件类型的支持之前,应该确保 TagLib 重构没有破坏已有的功能。
现在,可以确信仍可以上传图像。接下来就是添加对其他文件类型的支持,这只需在 switch 块中实现适当的 case。清单 14 演示如何处理上传的 HTML 文件,以及为默认的 case 创建一个链接来下载该文件:
清单 14. 完整的 switch/case 块
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 -->" } } } |
创建两个新的文本文件,以便测试这个新的行为:一个名为 test.html,另一个名为 noextension。将清单 15 中的内容添加到适当的文件中,上传该文件,确认 TagLib 是否按预期显示每个文件:
清单 15. 用于上传的两个示例文件
//test.html <p> This is some <b>test</b> HTML. </p> <p> Here is a link to the <a href="http://grails.org">Grails</a> homepage. </p> <p> And here is a link to the <img src="http://grails.org/images/grails-logo.png">Grails Logo</img>. </p> //noextension This file doesn't have an extension. |
Web 浏览器看上去应该如图 3 所示:
图 3. 显示所有 3 种类型的上传的文件
添加 Atom feed
至此,您应该明白一种截然不同的模式构成。对于添加到 Grails 应用程序的每个新特性,很可能都要涉及模型、视图和控制器。您还可以额外添加局部模板或 TagLib。
将 Atom feed 添加到 Blogito 同样也遵从该模式。虽然不要求更改模型,但最终要做所有其他的事情。您将:
- 在 Entry 控制器中添加一个闭包,以处理 Atom 请求。
- 创建一个新的 GSP 页面,用于以格式良好的 Atom 文档的形式呈现结果。
- 创建一个新的局部模板和一个新的定制标记,以加快进程。
本来您可以安装一个很好的 Feeds 插件,该插件可以为 Grails 应用程序添加 RSS 和 Atom 功能(请参阅 参考资料),但是我认为您将发现,Atom 格式非常简单,您自己完全可以应付。为证明这一点,您可以查看已有的 Atom feed 的源代码,或者查看关于 Atom 的 Wikipedia 页面最后的例子(请参阅 参考资料)。您甚至可以阅读 RFC 4287,这是用于 Atom 格式的 IETF 规范(请参阅 参考资料)。或者,您可以继续阅读本文,看看一个特定于 Grails 的解决方案。
首先,在 EntryController.groovy 中添加一个 atom 闭包,如清单 16 所示:
清单 16. 在 EntryController.groovy 中添加一个 atom 闭包
def atom = { if(!params.max) params.max = 10 def list = Entry.list( params ) def lastUpdated = list[0].lastUpdated [ entryInstanceList:list, lastUpdated:lastUpdated ] } |
这个闭包与标准的 list 闭包之间惟一的不同是增加了 lastUpdated 字段。由于该列表已经按 lastUpdated 排序(这要归因于 Entry domain 类的 static mapping 块中的 sort "lastUpdated":"desc" 设置),只需从该列表的第一个 Entry 中获取该字段,就可以得到最近的日期。
接下来,创建 grails-app/views/entry/atom.gsp。添加清单 17 中的代码:
清单 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> |
可以看到,以上代码做的第一件事是将 MIME 类型设置为 application/atom+xml。然后,提供关于该 feed 的一些基本的元数据:updated、author 和 generator 等。
如果想避免在整个 feed 中硬编码 blogito.org,那么可以让 atom 闭包获取 request.serverName,将它赋给一个变量,并在响应 hashmap 中返回它,同时返回的还有 entryInstanceList 和 lastUpdated。为了完全动态化,可以使用 request.scheme 返回 http,并使用 request.serverPort 返回 80。(唯一要避免使用 request.serverName 变量的地方是在 id 中,稍后我将讨论到这一点)。
对于 Atom feed 来说,以多种不同的格式提供链接并不少见。从 type 属性可以看出,该 feed 提供两个链接:一个是 HTML 链接,另一个是 Atom 格式的指向它本身的链接。self 链接特别有用;如果有一个不是自己下载的 Atom 文档,那么通过该链接就可以回溯到规范来源。
id 字段是 Atom feed 的惟一标识符,它不同于 URI 或可下载该 Atom feed 的当前位置。(您刚才已经知道,<link> 元素提供 feed 的当前来源)。在这个例子中,我使用 Mark Pilgrim 提供的技术生成一个惟一的、永久的 ID 字符串:将域名、feed 初次进入服务的日期和 URI 剩下的部分组合到一起。(要了解更多信息,请参阅 参考资料)。
id 的各个部分远不如整个字符串的惟一性重要。应确保这个 id 以后不会因为无意中传入来自控制器的变量而变化 — 对于 feed id,它应该既是惟一的,又是不变的。即使服务器的 address 发生变化,如果 feed 的内容不变,那么 feed id 也应该保持不变。
更新后的字段应该符合特定的格式 — 2003-12-13T18:30:02Z,或者确切地说是 RFC 3339。(要了解详细信息,请参阅 参考资料)。在已有的 grails-app/taglib/DateTagLib.groovy 文件中添加一个 atomDate 闭包,如清单 18 所示:
清单 18. 添加 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 } |
为了完成 Atom feed,创建 grails-app/views/entry/_atomEntry.gsp,并添加清单 19 中的代码:
清单 19. _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> |