2010年12月2日 星期四

JSP & Servlet 檔案上傳範例-使用Cos套件

Java EE規格雖然是針對網路應用開發的,但直到 Servlet 3.0 前都沒有提供檔案上傳的功能
在目前還沒打算轉換到Servlet 3.0 的打算下,先研究使用套件的解決方案
時下較為流行的上傳套件共有 Cos、FileUpload、SmartUpload
Cos套件是 O'reilly 公司提供,可至 http://www.servlets.com/cos 下載套件(cos-26Dec2008.zip)
FileUpload 的下載位置: http://commons.apache.org/fileupload/
FileUpload 使用須具備的套件:http://commons.apache.org/io/
SmartUpload 的網站已關閉,大概未來也不會繼續開發了

FileUpload 具有較多的功能,也有提供方便用來做 Ajax 應用的 listener
不過純以上傳效率來說Cos是最優秀的,贏過其他套件不少
這次實作選擇使用Cos

先提到上傳的介紹
撰寫上傳的功能必須先撰寫好一個頁面
並由表單以POST的形式將資料傳送到上傳處理的頁面
表單的編碼方式也與一般有所不同,enctype屬性共有三種值
1.application/x-www-form-urlencoded 是預設的編碼方式,它只處理表單域裡的value值
    採用這種編碼方式的表單會將表單域的值處理成URL編碼方式
2.multipart/form-data 編碼會以二進制流的方式來處理表單數據
   它把文件域指定文件的內容也封裝到請求參數里
   一旦設置了這種方式,就無法透過HttpServletRequest.getParameter()請求獲取請求參數
3.text/plain 編碼方式當表單的action屬性為mailto:URL的形式時比較方便
    這種方式主要適用於直接通過表單發送郵件的方式

這次實作的內容為撰寫一個提供圖片及附件上傳的功能
先寫好提供表單的網頁
designUploader.jsp======

.....
      <form action="UploadHandler" method="POST" enctype="multipart/form-data">
          <label>正面設計圖:</label><input type="file" id="f_pic" name="f_pic" value="" width="20" /><br />
          <label>背面設計圖:</label><input type="file" id="b_pic" name="b_pic" value="" width="20" /><br />
          <label>左側設計圖:</label><input type="file" id="l_pic" name="l_pic" value="" width="20" /><br />
          <label>右側設計圖:</label><input type="file" id="r_pic" name="r_pic" value="" width="20" /><br />
          <label>附件:</label><input type="file" id="appendix0" name="appendix0" value="" width="20" /><br />
          <input type="submit" value="Upload" /><br />
          <span>${uploadInfo.message}</span>
      </form>
....

解壓縮下載好的 Cos套件,並將 lib 裡的 cos.jar 複製到 WEB_INF 目錄底下的 lib 裡
接著撰寫處理上傳的頁面

使用Cos時,可以使用兩個類別來進行上傳工作:
1.  MultipartRequest     2.  MultipartParser

一般情況下使用MultipartRequest即可,不需要複雜的設定就可以輕鬆上傳物件
實際上MultipartRequest封裝了MultipartParser
在構造MultipartRequest實例時,建構了MultipartParser實例
建構 parser 的過程取得了上傳的 InputStream,但並不會真正讀取
然後透過 MultipartParser的readNextPart() method,從request流中讀取數據
區別出流中的參數域和文件域
如果是參數的話用ParamPart類封裝;如果是文件的話用FilePart封裝
此時如果設置了重命名策略的話,則在Server端新建一個新命名的空白物件
接著用FilePart的writeTo(saveDir)方法將流數據寫到硬碟中,文件上傳完成

MultipartRequest 範例:
UploadHandler.java=========

package tw.vencs;

import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.http.*;
import com.oreilly.servlet.MultipartRequest;

public class UploadHandler extends HttpServlet{

      public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
           //uploaded files' store dictionary
           String saveDirectory = .....
           //limit of file capacity
           int maxPostSize=5*1024*1024;

           String FileName=null;         
           //declare file type
           String ContentType=null;
         
           //count numbers of file uploaded
           int count = 0;

           MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8");
           //取得所有上傳之檔案輸入型態名稱及敘述
           Enumeration filename=multi.getFileNames();
           Enumeration filesdc=multi.getParameterNames();
         
           while(filename.hasMoreElements()){
                  String name=(String)filename.nextElement();
                  String dc=(String)filesdc.nextElement();
                  FileName=multi.getFilesystemName(name);
                  ContentType=multi.getContentType(name);
                  if(FileName!=null){
                   count++;
                  }
           }
           ............
      }
}

當 MultipartRequest 建立好的時候就是檔案上傳完畢的時候
不設定檔案上傳大小時,最大上傳限制預設是 1mb
如果上傳檔案大小超過設定的大小時會丟出例外
雖然看API的說明是會丟出 ExceededSizeException,不過結果似乎是裝在IOException


命名策略是指上傳的檔案遇到了檔名重複時的處理方式
有自訂處理方式可以自行撰寫,如下:
RandomFileRenamePolicy.java===========

package tw.vencs;

import java.io.File;
import java.util.Date;
import com.oreilly.servlet.multipart.FileRenamePolicy;

public class RandomFileRenamePolicy implements FileRenamePolicy {

    public File rename(File file) {
      String body="";
      String ext="";
      Date date = new Date();
      int pot=file.getName().lastIndexOf(".");
      if(pot!=-1){
          body= date.getTime() +"";
          ext=file.getName().substring(pot);
      }else{
          body=(new Date()).getTime()+"";
          ext="";
      }
      String newName=body+ext;
      file=new File(file.getParent(),newName);
      return file;

    }
}

這個命名規則會取得時間來做為隨機命名的依據,接著將MultipartRequest改寫成:
RandomFileRenamePolicy rfrp=new RandomFileRenamePolicy();
MultipartRequest multi = new MultipartRequest(request,saveDirectory,maxPostSize, "UTF-8",rfrp);

因為我實作的需求是希望能限制上傳檔案的大小
不管是怎麼樣的上傳套件都沒辦法再接收資料前得知檔案大小
畢竟那等同於Server端直接入侵Client讀取資料,權限上不會被允許
JavaScript可以做到檢查檔案大小的功能
<script language="JavaScript"> 
   
    //這裡控制要檢查的項目,true表示要檢查,false表示不檢查 
    var isCheckImageType = true;  //是否檢查圖片副檔名 
    var isCheckImageWidth = true;  //是否檢查圖片寬度 
    var isCheckImageHeight = true;  //是否檢查圖片高度 
    var isCheckImageSize = true;  //是否檢查圖片檔案大小 
   
    var ImageSizeLimit = 100000;  //上傳上限,單位:byte 
    var ImageWidthLimit = 1200;  //圖片寬度上限 
    var ImageHeightLimit = 1000;  //圖片高度上限 
   
    function checkFile() { 
        var f = document.FileForm; 
        var re = /\.(jpg|gif)$/i;  //允許的圖片副檔名 
        if (isCheckImageType && !re.test(f.file1.value)) { 
            alert("只允許上傳JPG或GIF影像檔"); 
        } else { 
            var img = new Image(); 
            img.onload = checkImage; 
            img.src = f.file1.value; 
        } 
    } 
    function checkImage() { 
        if (isCheckImageWidth && this.width > ImageWidthLimit) { 
            showMessage('寬度','px',this.width,ImageWidthLimit); 
        } else if (isCheckImageHeight && this.height > ImageHeightLimit) { 
            showMessage('高度','px',this.height,ImageHeightLimit); 
        } else if (isCheckImageSize && this.fileSize > ImageSizeLimit) { 
            showMessage('檔案大小','kb',this.fileSize/1000,ImageSizeLimit/1000);         
        } else { 
            document.FileForm.submit(); 
        } 
    } 
    function showMessage(kind,unit,real,limit) { 
        var msg = "您所選擇的圖片kind為 real unit\n超過了上傳上限 limit unit\n不允許上傳!" 
        alert(msg.replace(/kind/,kind).replace(/unit/g,unit).replace(/real/,real).replace(/limit/,limit)); 
    } 
</script>


不過預防Client端沒開啟JavaScript的情形,我再查了些資料
java.io套件裡的 FileInputStream 有available() method可以檢查request送來的資料流大小
而不必等到資料流全傳輸完畢
可惜的是看過 FileInputStream 的API後知道available()的限制
它處理的是沒遭遇到network blocking時所送來的一串資料流,只是現實上常有blocking發生...
MultipartRequest最多是在全部的物件上傳完後再來檢查
既然傳輸資料的過程已經免不了了,那我希望至少是當一個檔案上傳時就先檢查
而省掉全傳輸完所耗費的資源及時間
所以將 UploadHandler 改寫成以 MultipartParser 處理
UploadHandler.java=========

package tw.vencs;

import java.io.File;
import java.io.IOException;
import javax.servlet.http.*;
import com.oreilly.servlet.multipart.FilePart;
import com.oreilly.servlet.multipart.MultipartParser;
import com.oreilly.servlet.multipart.Part;

public class UploadHandler extends HttpServlet{

      public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
          HttpSession session = request.getSession();
         
          //uploaded files' store dictionary
          String saveDirectory = null;
          //try &catch to prevent login session has been canceled
         
          File dir = new File(saveDirectory);
         
          int pictureSize = Integer.parseInt(getServletConfig().getInitParameter("pictureSize"));
          int appendixSize = Integer.parseInt(getServletConfig().getInitParameter("appendixSize"));
          int maxPostSize=pictureSize*4 + appendixSize;         //limit of file capacity
         
          String Message = "";
          
              MultipartParser fileMap = null;
              String fileName = null;              
            
              try{
                  fileMap = new MultipartParser(request, maxPostSize);
                  fileMap.setEncoding("UTF-8");

                  Part part = null;
             
                   while ((part = fileMap.readNextPart()) != null){
                       if(part.isFile()){         //if this inputstream is from HTML file upload element
                           FilePart filePart = (FilePart) part;
                           filePart.setRenamePolicy(new DefaultFileRenamePolicy());
                           String name = part.getName();        //get HTML elements' name of form
                           fileName = filePart.getFileName();   //get name of uploaded object
                         
                           if(fileName != null && !fileName.equals("")){                          
                             
                               String fileType = fileName.substring(
                                                   fileName.lastIndexOf('.')+1).toLowerCase();  //get file type
                               String tempMessage = null;

                               if(name.substring(2).equals("pic")){
                                   File dir = new File(saveDirectory);
                                   dir.mkdirs();
                                   //write data into designated file directory
                                   long size = filePart.writeTo(dir);

                                 
                                   if(fileType.equals("jpg") || fileType.equals("jpeg") ||
                                     fileType.equals("png") || fileType.equals("bmp") ||
                                     fileType.equals("gif") || fileType.equals("ai")){
                                     
                                      if(size > (long)pictureSize){
                                          tempMessage = fileName + "大小超過限制!!<br />";
                                          Message += (tempMessage);
                                          File disposedFile = new File(
                                                                             saveDirectory + "/" + filePart.getFileName());
                                          disposedFile.delete();
                                      }else{
                                          session.setAttribute(
                                                         name, saveDirectory + "/" + filePart.getFileName());
                                          tempMessage = fileName + "上傳成功!!<br />";
                                          Message += tempMessage;
                                      }
                                        
                                   }else{
                                       tempMessage = fileName + "不是圖檔格式!!<br />";
                                       Message += tempMessage;
                                       File disposedFile = new File(saveDirectory + "/" + fileName);
                                       disposedFile.delete();
                                   }
                               }else{                                        //appendix check
                                  File appendDir = new File(saveDirectory + "/appendix");
                                  appendDir.mkdirs();
                                  int size = (int)filePart.writeTo(appendDir);
                                  int uAppendSize = 0;                    //store uploaded data size info
                                
                                  if(session.getAttribute("append") != null &&
                                    !session.getAttribute("append").equals("")){
                                    
                                      File[] fList = appendDir.listFiles();
                                         for (int j = 0; j < fList.length; j++){
                                             FileInputStream in = new FileInputStream(fList[j]);
                                             uAppendSize += in.available();
                                             in.close();         //must close FileInputStream after use it
                                         }
                                  }
                                 
                                    if(size > (appendixSize - uAppendSize)){
                                        tempMessage = fileName + "大小超過限制!!<br />";
                                        Message += tempMessage;
                                        File disposedFile = new File(
                                               saveDirectory + "/appendix/" + filePart.getFileName());
                                        disposedFile.delete();  
                                    }else{
                                        String temp = "";
                                        if(session.getAttribute("append")!= null &&
                                           !session.getAttribute("append").equals("")){
                                            temp = session.getAttribute("append").toString();
                                        }
                                        temp += (saveDirectory +
                                            "/appendix/" + fileName + "=" + filePart.getFileName() + ",");
                                        session.setAttribute("append", temp);
                                      
                                        tempMessage = fileName + "上傳成功!!<br />";
                                        Message += tempMessage;
                                    }
                               }
                           }
                      }
                   }
              }catch(IOException e){
                  session.setAttribute("targetPage", "designUploader.jsp");
                  session.setAttribute("Message", "上傳檔案總大小超過限制!!<br />");
                  response.sendRedirect("designUploader.jsp");
                  return;
              }
              session.setAttribute("targetPage", "designUploader.jsp");
              session.setAttribute("Message", Message);
              response.sendRedirect("designUploader.jsp");
              return;
          }
      }
}

預設的命名規則是遇到覆蓋已存在的同名檔案
因為我想處理的只有上傳域的物件
假使表單裡還有其他文件域的資訊,如下處理:
 if (part.isParam()){              //if this inputstream is from normal HTML input element       
       ParamPart paramPart = (ParamPart) part;
       String value = paramPart.getStringValue();  //get param's value

沒有留言:

張貼留言