2010年12月11日 星期六

[轉載] [FCKeditor] 用 Java Taglib 使用 FCKeditor

FCKeditor 是所見即所得(WYSIWYG)的線上編輯器, 支援多國語言, 多種瀏覽器支援, 可更換SKIN,還可以用JavaScript控制按鈕,功能十分多樣化,外掛的功能也是十分強大,支援瀏覽器管理檔案...等多樣化外掛。

支援語言: ASP.Net, ASP, ColdFusion, PHP, Java, Active-FoxPro, Lasso, Perl, Python
支援流覽器: IE, FireFox, Safari, Opera, Google Chrome, Camino
支援平台: MS Windows , MAC , Linux
版權: under the GPL, LGPL and MPL open source licenses

在 Java 的平台上有一個很好用的工具 Taglib , 可以讓你的 jsp 程式簡單化.配置如下

FCKeditor: FCKeditor_2.6.4.zip
FCKeditor.Java: fckeditor-java-2.4.1-bin.zip
FCKeditor.Java 文件參考: http://java.fckeditor.net/
SLF4j: slf4j-1.5.6.zip
Application Server: Tomcat 6.x.x

step01:
在 WebContent 目錄下建立一個目錄 fckeditor , 並將 FCKeditor_2.6.4.zip 解開的資料放在這個目錄下.

step02:
解 開 fckeditor-java-2.4.1-bin.zip , 將 fckeditor-java-core-2.4.1.jar, 及 lib 目錄下的三個檔案 commons-fileupload-1.2.1.jar, commons-io-1.3.2.jar, slf4j-api-1.5.2.jar , 複製到 WEB-INF\lib

step03:
解開 slf4j-1.5.6.zip , 將slf4j-api-1.5.6.jar, slf4j-simple-1.5.6.jar 複製到 WEB-INF\lib

step04:
在 WEB-INF\web.xml 檔增加下列程式碼
<servlet>
 <servlet-name>Connector</servlet-name>
 <servlet-class>net.fckeditor.connector.ConnectorServlet</servlet-class>
 <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Connector</servlet-name>
  <url-pattern> /fckeditor/editor/filemanager/connectors/*
  </url-pattern>
</servlet-mapping>


step05:
在 WEB-INF 目錄下增加一個檔案 FCKeditor.tld , 內容如下:
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
 http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
 version="2.0">
 <description>
  The FCKeditor Tag Library offers a very convenient way to create
  several FCKeditor instances with different configurations.
  Additionally, you can check for user-based capabilities.
 </description>
 <display-name>FCKeditor Tag Library</display-name>
 <tlib-version>2.4</tlib-version>
 <short-name>FCK</short-name>
 <uri>http://java.fckeditor.net</uri>
 <tag>
  <description>
   Creates a FCKeditor instance with the given parameters. Any
   parameter except instanceName which is empty or contains
   whitespaces only will be ignored.
  </description>
  <display-name>editor</display-name>
  <name>editor</name>
  <tag-class>net.fckeditor.tags.EditorTag</tag-class>
  <body-content>JSP</body-content>
  <attribute>
   <description>
    The unique instance name under which the editor can be
    retrieved through the API.
   </description>
   <name>instanceName</name>
   <required>true</required>
   <rtexprvalue>true</rtexprvalue>
   <type>java.lang.String</type>
  </attribute>
  <attribute>
   <description>
    Width of the FCKeditor instance in the browser window.
   </description>
   <name>width</name>
   <rtexprvalue>true</rtexprvalue>
   <type>java.lang.String</type>
  </attribute>
  <attribute>
   <description>
    Height of the FCKeditor instance in the browser window.
   </description>
   <name>height</name>
   <rtexprvalue>true</rtexprvalue>
   <type>java.lang.String</type>
  </attribute>
  <attribute>
   <description>
    The toolbar set which shall be displayed to the user.
   </description>
   <name>toolbarSet</name>
   <rtexprvalue>true</rtexprvalue>
   <type>java.lang.String</type>
  </attribute>
  <attribute>
   <description>
    The path/folder in which the editor is deployed under
    the given context. The context path will be attached
    automatically. (e.g. '/fckeditor')
   </description>
   <name>basePath</name>
   <rtexprvalue>true</rtexprvalue>
   <type>java.lang.String</type>
  </attribute>
  <attribute>
   <description>
    Passes any content to the FCKeditor document. Use the
    jsp:attribute tag for large inline content. \r, \n, and
    \t will be truncated.
   </description>
   <name>value</name>
   <rtexprvalue>true</rtexprvalue>
   <type>java.lang.String</type>
  </attribute>
  <example><![CDATA[
<FCK:editor instanceName="editorDefault" height="500px" />]]>
  </example>
 </tag>
 <tag>
  <description>
   Sets a config property of the editor to the supplied value.
   You may provide any attribute you want for the editor. Set
   at least one attribute per tag or several attributes with
   one tag. This tag can only be nested within an editor tag.
   For all configuration options click
   <![CDATA[<a href="http://docs.fckeditor.net/FCKeditor_2.x/Developers_Guide/Configuration/Configuration_Options">here</a>]]>.
  </description>
  <display-name>config</display-name>
  <name>config</name>
  <tag-class>net.fckeditor.tags.ConfigTag</tag-class>
  <body-content>empty</body-content>
  <dynamic-attributes>true</dynamic-attributes>
  <example>
   <![CDATA[
<FCK:config SkinPath="/skins/silver/" AutoDetectLanguage="true" />]]>
  </example>
 </tag>
 <tag>
  <description>
   Displays session-dependent and compatibility-related
   information. This tag is intended for developers only.
   Response messages cannot be localized, they are English
   only.
  </description>
  <display-name>check</display-name>
  <name>check</name>
  <tag-class>net.fckeditor.tags.CheckTag</tag-class>
  <body-content>empty</body-content>
  <attribute>
   <description>
    Provide the feature name you want to check. Valid
    features are [FileUpload, FileBrowsing,
    CompatibleBrowser]
   </description>
   <name>command</name>
   <required>true</required>
   <type>java.lang.String</type>
  </attribute>
  <example><![CDATA[
<FCK:check command="FileUpload" />
<FCK:check command="CompatibleBrowser" />]]>
  </example>
 </tag>
</taglib>


step06:
建立一個 fck.jsp 來測試, 程式碼如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="/WEB-INF/FCKeditor.tld" prefix="FCK"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>

<form action="show.jsp" method="post" target="_blank">
<FCK:editor instanceName="fckEdit"  width="700" height="500"  toolbarSet="Default" />
<input type="submit" value="Submit">
</form>


</body>
</html>


fckeditor-java-2.4.1 的設定比較簡便, 就連上載檔案, 也只要在step04: 設定好 servlet : Connector 就可以了. 不需要太繁瑣. 不過若要上載檔案, 不要忘了要在 classpath 建立一個檔案 fckeditor.properties , 內容如下:
fckeditor.toolbarSet=Basic
connector.userActionImpl=net.fckeditor.requestcycle.impl.UserActionImpl
connector.userFilesPath=/userfiles
connector.resourceType.file.extensions.allowed=txt|doc
connector.resourceType.image.extensions.allowed=jpg|png


這樣才不會有問題. 參數說明:
connector.userFilesPath : 是伺服器上要存放檔案的地方.如 /userfiles
connector.resourceType.file.extensions.allowed : 可以限制上傳到/userfiles/file 目錄的附加檔名有那些
connector.resourceType.image.extensions.allowed: 可以限制上傳到/userfiles/image 目錄的附加檔名有那些

因為上載的檔案只能存在這個 application WEBcontent 的目錄內, 所以若將 java 打包成 war 檔在 JBoss 上使用, 則會有問題.

2010年12月10日 星期五

非英數名稱圖片顯示處理方案

這幾天在利用之前寫得上傳套件上傳圖片時遇到了問題(感謝專案組員抓到這個bug)
那就是上傳的圖片若以中文、日文等非英文、數字的名稱上傳遇到麻煩
在WebApplication會顯示找不到檔案的錯誤訊息

我先試著將上傳圖片名稱改成URL上的UTF-8編碼
String fileName = java.net.URLEncoder.encode( file.getName(), "UTF-8");
file.renameTo (new File(file.getParent(),   fileName));

雖然檔名的確與URL上經過編碼的檔案名稱相符,不過還是一樣找不到檔案... = =

既然上傳有問題,那剩下的方法就是禁止上傳檔名有編碼問題的檔案囉
最後做法是用正規表示法協助,讀取上傳檔案的檔名加以判斷
有問題的檔案就直接刪除了,並回覆訊息請求上傳有正確檔名的檔案
// 判斷是否含有非英數編碼的字元
String stringEncodeJudge = "^.*[^0-9a-zA-Z~!@#\\$%\\^&\\*\\(\\)_\\+\\{\\}\\|:\\\"<>\\?\\[\\];',\\.\\/].*$";

    if(!filePart.getFileName().matches(stringEncodeJudge)){
         session.setAttribute(name, saveDirectory + "/" + filePart.getFileName());
         tempMessage = fileName + "上傳成功!!<br />";
         Message += tempMessage;
    }else{
         tempMessage = fileName + "請改成英文、數字組成的檔名!!<br />";
         Message += (tempMessage);
         File disposedFile = new File(saveDirectory + "/" + filePart.getFileName());
         disposedFile.delete();
   }

2010年12月8日 星期三

範例 - 刪除目錄及其底下所有文件

      public void deleteAll(File path){
          if(path.exists()){
              if(path.isFile()){ 
                  path.delete(); 
              }else{ 
                  File[] files = path.listFiles();     
                  for(int i = 0; i < files.length; i++){ 
                        deleteAll(files[i]); 
                  }
 
                  path.delete(); 
              }
          }
      }

範例 - 計算硬碟中的資料大小

int uAppendSize = 0;                                     //store uploaded data size info
                                     
File[] fList =TargetDir.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
}

這個範例會檢查寫入檔案所在的資料夾,並加該資料夾內的所有檔案大小相加
FileInputStream 開啟後務必要記得關閉
否則檔案的刪除、修改等動作會因為檔案被FileInputStream佔用而失敗

2010年12月7日 星期二

[轉載] 使用response.sendRedirect的注意事項

首先我們要明白用response.sendRedirect做轉向的原理
它其實是向瀏覽器發送一個特殊的Header,然後由瀏覽器來做轉向,轉到指定的頁面
所以用sendRedirect時,瀏覽器的地址欄上可以看到地址的變化
用<jsp:forward page=""/>則不同,它是直接在server做的
瀏覽器並不知道,也不和瀏覽器打交道,這從瀏覽器的地址並不變化可以看出。
所以使用response.sendRedirect時就需要注意以下兩點:

1,在使用response.sendRedirect時,前面不能有HTML輸出
這並不是絕對的,不能有HTML輸出其實是指不能有HTML被送到了瀏覽器
事實上現在的server都有cache機制,一般在8K(我是說JSP SERVER)
這就意味著,除非你關閉了cache,或者你使用了out.flush()強制刷新
那麼在使用sendRedirect之前,有少量的 HTML輸出也是允許的
如果報錯說,「一些信息已經被submitted」(原文忘了)
那麼,你就要注意看了,前面是不是有過多的HTML輸出了

2,在response.sendRedirect之後,應該緊跟一句return;
我們已經知道response.sendRedirect是通過瀏覽器來做轉向的
所以只有在頁面處理完成後,才會有實際的動作
既然你已經要做轉向了,那麼後的輸出還有什麼意義呢?
而且有可能會因為後面的輸出導致轉向失敗

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

2010年12月1日 星期三

[轉載] HttpClient 4.x 表單檔案上傳

最近手邊的專案漸漸由 HttpClient 3.x 轉移至改用 HttpClient 4.x 實作 Http 操作。
使用 Commons 專案的東西,捨棄原始的 HttpURLConnection 就是為了較輕鬆地處理 Http 操作的功能。

我們可以用下列方式透過網頁表單上傳檔案:

HttpClient client = new DefaultHttpClient();
HttpPost post =new HttpPost("http://example.com/upload.do");
MultipartEntity e = new MultipartEntity();
try{
        e.addPart("field1", new StringBody("some value"));
        e.addPart("file", new FileBody(new File("your_file_path")));
}catch (UnsupportedEncodingException ex) {
        ex.printStackTrace();
}

post.setEntity(e);
client.execute(post);

它的寫法是相當容易的!不過有個小缺點,那就是 HttpClient 4.x 某個版本將 Multipart Entity 實作獨立至 HttpMime 專案
http://hc.apache.org/httpcomponents-client/httpmime/index.html
所以,您要使用單純的表單上傳功能,就需要同時再引用 HttpMime 相關的 Library

C:\http-mime>dir
    磁碟區 C 中的磁碟沒有標籤。
    磁碟區序號: A8B9-DC3D

C:\http-mime 的目錄

2010/01/01 下午 01:20 <DIR> .
2010/01/01 下午 01:20 <DIR> ..
2009/12/31 下午 02:22 345,048 apache-mime4j-0.6.jar
2009/12/31 下午 02:22 25,993 httpmime-4.0.jar
2009/12/31 下午 02:22 2,255 jcip-annotations-1.0.jar
                    3 個檔案 373,296 位元組
                    2 個目錄 70,042,320,896 位元組可用

雖然,這些 Library 僅 300 多 kb,對一般的應用程式開發來說並不算什麼。
但總覺得是否能讓它單純一些呢?我們並不需要完整而強大的 Http Mime,
有時我們只需要足夠的功能罷了!隨著這個想法,試著實作一個極簡版本的 ThinMultipartEntity。
可以不需引用 HttpMime 就上傳檔案。

package com.google.jplurk.net;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Random;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.message.BasicHeader;

public class ThinMultipartEntity implements HttpEntity {

  static Log logger = LogFactory.getLog(ThinMultipartEntity.class);

  private final static char[] MULTIPART_CHARS =
"-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
      .toCharArray();

  private String boundary = null;

  ByteArrayOutputStream out = new ByteArrayOutputStream();
  boolean isSetLast = false;
  boolean isSetFirst = false;

  public ThinMultipartEntity() {
    StringBuffer buf = new StringBuffer();
    Random rand = new Random();
    for (int i = 0; i < 30; i++) {
      buf.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
    }
    this.boundary = buf.toString();

  }

  public void writeFirstBoundary(){
    if(!isSetFirst){
      try {
        out.write(("--" + boundary + "\r\n").getBytes());
      } catch (IOException e) {
        logger.error(e.getMessage(), e);
      }
    }
    isSetFirst = true;
  }

  public void writeLastBoundary() {
    if(isSetLast){
      return ;
    }
    try {
      out.write(("\r\n--" + boundary + "--\r\n").getBytes());
    } catch (IOException e) {
      logger.error(e.getMessage(), e);
    }
    isSetLast = true;
  }

  public void addPart(String key, String value) {
    writeFirstBoundary();
    try {
      out.write(("Content-Disposition: form-data; name=\"" +key+"\"\r\n").getBytes());
      out.write("Content-Type: text/plain; charset=UTF-8\r\n".getBytes());
      out.write("Content-Transfer-Encoding: 8bit\r\n\r\n".getBytes());
      out.write(value.getBytes());
      out.write(("\r\n--" + boundary + "\r\n").getBytes());
    } catch (IOException e) {
      logger.error(e.getMessage(), e);
    }
  }

  public void addPart(String key, File value) {
    writeFirstBoundary();
    try {
      out.write(("Content-Disposition: form-data; name=\""
+ key+"\"; filename=\"" + value.getName()+ "\"\r\n").getBytes());
      out.write("Content-Type: application/octet-stream\r\n".getBytes());
      out.write("Content-Transfer-Encoding: binary\r\n\r\n".getBytes());

      FileInputStream fin = new FileInputStream(value);
      int data = fin.read();
      while(data !=-1){
        out.write(data);
        data = fin.read();
      }

    } catch (IOException e) {
      logger.error(e.getMessage(), e);
    }
  }

  public long getContentLength() {
    writeLastBoundary();
    return out.toByteArray().length;
  }

  public Header getContentType() {
    return new BasicHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
  }

  public boolean isChunked() {
    return false;
  }

  public boolean isRepeatable() {
    return false;
  }

  public boolean isStreaming() {
    return false;
  }

  public void writeTo(OutputStream outstream) throws IOException {
    outstream.write(out.toByteArray());
  }

  public Header getContentEncoding() {
    return null;
  }

  public void consumeContent() throws IOException,
      UnsupportedOperationException {
    if (isStreaming()) {
      throw new UnsupportedOperationException(
          "Streaming entity does not implement #consumeContent()");
    }
  }

  public InputStream getContent() throws IOException,
      UnsupportedOperationException {
    throw new UnsupportedOperationException(
        "Multipart form entity does not implement #getContent()");
  }

}

PS. 因為是極簡版的,有許多細節是被省略的
PS. 只試過上傳一個檔 XD