在這一期的 精通 Grails 中,Scott Davis 展示如何將文件上傳到 Grails 應用程序,並設置一個 Atom syndication feed。完成最後這些部分之後,Blogito 便成為一個完整的博客伺服器。
在過去幾期的 精通 Grails 文章中,您一直在逐步構建一個小型的博客服務(Blogito)。在這篇文章中,Blogito 將最終完工,成為一個實用的博客應用程序。您將為博客條目主體實現文件上傳功能,並添加自己製作的用於聚合的 Atom feed。
|
但是,在開始之前,請注意在上一篇文章(“身份驗證和授權”)中,我加入的認證使 UI 中出現一個細小的 bug。在加入新的特性之前,應該修復這個 bug。
修復隱藏的 bug
啟動 Grails 時,grails-app/conf/Bootstrap.groovy 增加 2 個用戶和 4 個新的博客條目。但是,如果嘗試通過 Web 界面增加博客條目,會發生什麼?可以使用下面的步驟試試看:
您將看到以下錯誤:Property [author] of class [class Entry] cannot be null。那麼,這個 bug 是如何引入到應用程序中的?畢竟,bootstrap 代碼還能正常工作。
在第一篇 Blogito 文章(“改變 Grails 應用程序的外觀”)中,我讓您通過輸入 grails generate-views Entry 生成 Groovy Server Pages(GSP)視圖。在隨後的文章中,我更改了 domain 類,但是從未讓您再回過頭來生成視圖。當我添加 Entry 與 User 之間的 1:M 關係時,磁碟上的 create.gsp 視圖一直不變,如清單 1 所示。(還記得嗎,belongsTo 創建一個名為 author 的欄位,該欄位的類型為 User)。
class Entry { static belongsTo = [author:User] String title String summary Date dateCreated Date lastUpdated } |
不得不說,要使一切同步,最安全的方式還是通過動態腳手架生成視圖 — 特別是在開發的早期,域模型不斷變化的時候,更是如此。當然,不能僅僅依靠通過腳手架生成的視圖,但是,當您在磁碟上生成 GSP 時,使它們保持最新的責任就從 Grails 轉移到您自己身上。
如果現在為 Entry 類生成視圖的話,Grails 會提供一個組合框,其中顯示一個 Author 列表,如清單 2 所示。您自己不要 這樣做 — 這只是為了演示。稍後我將提供兩種不同的選項。
<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> |
注意 <g:select> 元素。欄位名為 author.id。在 “GORM - 有趣的名稱,嚴肅的技術” 中可以了解到,列表中顯示的文本來自 User.toString() 方法。該文本通常也是表單提交時作為欄位值發回到伺服器的值。在這裡,optionKey 屬性覆蓋欄位值,從而發回 Author 的 id。(要了解更多關於 <g:select> 標記的信息,請參閱 參考資料)。
為 EntryController.groovy 提供 author.id 欄位的最快方式是將一個隱藏欄位添加到表單中,如清單 3 所示。由於執行 create 動作前必須登錄,而登錄的 User 是博客條目的 author,因此對於這個值可以安全地使用 session.user.id。
<g:form action="save" method="post" > <input type="hidden" name="author.id" value="${session.user.id}" /> <!-- SNIP --> </g:form> |
對於像 Blogito 這樣的簡單的應用程序,這樣也許就足夠了。但是,這樣做留下了一個漏洞,使客戶端的黑客有機會為 author.id 注入不同的值。為確保徹底的安全,可以在 save 閉包中添加 Entry.author,如清單 4 所示:
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]) } } |
這是生成控制器時得到的標準 save 閉包,再加上一行定製的代碼。entryInstance.author 行根據 session.user.id 值從資料庫獲取 User,並填充 Entry.author 欄位。
在下一節中,您將定製 save 閉包,以處理文件上傳,所以您仍可能在安全性方面犯錯誤,將 清單 4 中的代碼添加到 EntryController.groovy 中。重新啟動 Grails,確保可以通過 HTML 表單成功地添加新的 Entry。
文件上傳
現在又可以創建 Entry,接下來該添加另一個特性。我希望用戶在創建新的 Entry 時可以上傳文件。這種文件可以是包含整個博客條目的 HTML,也可以是圖像或任何其他文件。為實現該特性,需要涉及到 Entry domain 類、EntryController 和 GSP 視圖 — 並且要增加一個新的 TagLib。
首先,看看 grails-app/views/entry/create.gsp。添加一個新欄位,用於上傳文件,如清單 5 所示:
<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> |
注意,<g:form> 標記已經被改為 <g:uploadForm>。這樣便支持從 HTML 表單上傳文件。實際上,也可以保留 <g:form> 標記,並增加一個 enctype="multipart/form-data" 屬性。(用於 HTML 表單的默認 enctype 是 application/x-www-form-urlencoded)。
如果正確設置了表單的 enctype(或者使用 <g:uploadForm>),就可以添加 <input type="file" /> 欄位。這樣便為用戶提供了一個按鈕,用於瀏覽本地文件系統,並選擇上傳的文件,如圖 1 所示。我的例子使用 Grails 徽標;您也可以使用任何自己喜歡的圖像。
現在,客戶端表單已經做好了,接下來可以調整伺服器端代碼,以便用上傳的文件做有用的事情。在文本編輯器中打開 grails-app/controllers/EntryController.groovy,將清單 6 中的代碼添加到 save 閉包中:
def save = { def entryInstance = new Entry(params) entryInstance.author = User.get(session.user.id) //handle uploaded file 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]) } } |
[火星人 ] 精通 Grails: 文件上傳和 Atom 聯合已經有648次圍觀