2008年5月3日 星期六

[Struts] 建立一個可以讓使用者下載檔案的 Action

Struts 的 Action 扮演 MVC(Model-View-Controller) 中的 Controller 角色

所有 Struts 的 request 都會先經過 Controller 在進行後端的 Model 計算並產生前端的 View

相信對於使用 Struts 的 programmer 來說,一般的 Action 都是不陌生的!!

但是,要如何可以讓使用者透過瀏覽器來下載檔案呢??

當然這檔案不是一個 static file,要對使用者的 request 產生一個 dynamic file

static file要做到就是直接將路徑指到 browser 的 URL 就OK

那 dynamic file呢?

Struts 提供一種 Action - DownloadAction,就如同一般的 Action 使用一樣

但是不同點在於需要 implement 一個 getStreamInfo method

所以我的 Controller 就會長成這樣:

public class MyController extends DownloadAction {
    public StreamInfo getStreamInfo(...) {
    }
}

基本的 class 架構就如上面的樣子,那 getStreamInfo method 呢?? 我要怎樣去完成??

我們先討論為何會有 getStreamInfo method 的存在

這個 method 是讓我們可以撰寫要讓使用者下載檔案的內容

觀察 Struts 的 DownloadAction source code 就可以發現,DownloadAction 是如何執行的:

1.首先呼叫我們撰寫的 getStreamInfo() method 來建立一個 StreamInfo object

2.接著就是由 StreamInfo object 中獲得 content type and inputStream

3.再來就是設定 browser 的 content type,告知 browser 要如何處理該檔案,如果是可以由外部存取的檔案,就會試著呼叫相關的 application 來支援

4.最後就是將 inputStream 的內容 copy 到 response 的 outputStream

使用者就可以透過 browser 取得檔案或是呼叫相關的 application 來處理檔案

知道流程後,我們就必須要撰寫 getStreamInfo() method 來符合流程囉~~

由上述的流程中,我們需要建立一個 StreamInfo object,裡面包含 content type and inputStream,所以重點就在於 StreamInfo object 的撰寫

觀察 Struts API 就會發現我們竟然找不到 StreamInfo interface 的內容!! 該不會 API 出錯??

身為一個 JAVA Programmer 的直覺,StreamInfo 一定是 DownloadAction 的 inner interface,所以我們要改查 DownloadAction.StreamInfo,果然不出我所料!!

既然是一個 interface 那我們就要接著觀察他有哪些 Known Implementing Classes

在 API 中立即就寫出:
All Known Implementing Classes:
    DownloadAction.FileStreamInfo, DownloadAction.ResourceStreamInfo


所以我們就要觀察哪個 implementing class 是我們所需要的!!

從 DownloadAction.FileStreamInfo 中知道這是用來產生檔案下載的,而 DownloadAction.ResourceStreamInfo 是用來產生網路資源的

所以我們就鎖定 DownloadAction.FileStreamInfo class 來使用

決定好關鍵的 class 之後,就是開始撰寫我們 getStreamInfo() method 囉~~

然而,需要建立一個 DownloadAction.FileStreamInfo 物件就需要設定他的 content type and file object(因為 constructor 的 parameter)

所以我們就要先建立一個 file 物件來讓 DownloadAction 的 inputStream 可以完成第四項流程

我們需要將我們的 dynamic file 備份到 local 的 HD 中,再將我們存放的路徑給予 File object

所以我們的 getStreamInfo() method 會先有這樣的內容:

public StreamInfo getStreamInfo(...) {
    String path = "/a.dat";
    File file = new File(path);
}

再來就是 content type 的部分,content type 就是告知 browser 該如何去處理接著要下載的檔案,例如我們設定 application/pdf,browser 就會預設呼叫 Adobe Acrobat Reader 來讀取

但是這樣還是無法完成正確的下載,所以我們必須在 StreamInfo 物件送交之前,對 response 設定 Header 為 "Content-disposition",value 為 "attachment; filename=a.dat"

這段設定的目的在於,強制 browser 不要在 browser 本身開啟檔案,強制 browser 透過下載檔案的方式來處理,而 filename 的部分卻可以讓我們對於使用者下載的檔名給與預設值

所以最後完成的內容應該會包含這樣的樣子:
public class MyController extends DownloadAction {
    public StreamInfo getStreamInfo(...) {

        String path = "/a.dat";
        File file = new File(path);
        String contentType = "application/unknow";
        //商業邏輯...
        response.setHeader(
            "Content-disposition",
            "attachment; filename=a.dat");
        return new FileStreamInfo(contentType,file);

    }
}

這樣我們就可以完成讓使用者下載檔案的功能了!!

沒有留言: