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 

2010年11月27日 星期六

[轉載] Apache HttpClient 4.x 使用 GET, POST 範例

先前我曾經發了一篇介紹如何抓取網頁的教學:
利用Jakarta.Commons.HttpClient抓取網頁、網站(Parser),不過當時用的是 HttpClient 3.1。
經過時間的演進 Apache 已經在 14 August 2009 發佈HttpComponents HttpClient 4.0 (GA)
由 3.1 到 4.0 因為底層幾乎全部重新改寫,所以也使有些舊的程式無法使用。
這篇就是我自己寫的一個簡單範例。

在看範例之前先把一些重要連結整理給大家:
想知道這次到底更動了哪些東西可以看:Apache HttpClient 首頁
官方的 Tutorial 在:Apache HttpClient Tutorial
而 API DOC、說明文件則在:Apache HttpClient apidocs

相關的程式碼、jar 檔在:HttpComponents HttpClient 4.0 (GA)
注意,在寫程式前必需先將四個 jar 檔正確匯入,最後兩個(*)是選用,
請參考:http://hc.apache.org/httpcomponents-client-ga/quickstart.html

  • commons-logging-x.x.x.jar
  • commons-codec-x.x.x.jar
  • httpcore-x.x.x.jar
  • httpclient-x.x.x.jar
  • apache-mime4j-x.x.x.jar (*)
  • httpmime-x.x.x.jar (*)
說了這麼多,以下是程式的範例,
第一個是傳回在 google 查詢 httpclient 的結果。
第二則是傳回台大圖書館查詢 Head First Java 的結果。

 package demo.httpclient;

import java.io.IOException;
import java.util.ArrayList;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

/**
* Apache HttpClient 4.x 使用 GET, POST 查詢網頁的範例
*
* @author werdna at http://werdna1222coldcodes.blogspot.com/
*/

public class HttpClientDemo extends DefaultHttpClient {

public static void main(String[] args) throws IOException {

DefaultHttpClient demo = new DefaultHttpClient();
demo.getParams().setParameter("http.protocol.content-charset", "UTF-8");

// Get Request Example,取得 google 查詢 httpclient 的結果
HttpGet httpGet = new HttpGet("http://www.google.com.tw/search?q=httpclinet");
HttpResponse response = demo.execute(httpGet);
String responseString = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 如果回傳是 200 OK 的話才輸出
System.out.println(responseString);
} else {
System.out.println(response.getStatusLine());
}

// Post Request Example,查詢台大圖書館書籍
ArrayList<NameValuePair> pairList = new ArrayList<NameValuePair>();
pairList.add(new BasicNameValuePair("searchtype", "t"));
pairList.add(new BasicNameValuePair("searchscope", "keyword"));
pairList.add(new BasicNameValuePair("searcharg", "Head First Java"));
pairList.add(new BasicNameValuePair("SORT", "D"));

HttpPost httpPost = new HttpPost("http://tulips.ntu.edu.tw:1081/search*cht/a?");
StringEntity entity = new StringEntity(URLEncodedUtils.format(pairList, "UTF-8"));
httpPost.setEntity(entity);
response = demo.execute(httpPost);
responseString = EntityUtils.toString(response.getEntity());
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 如果回傳是 200 OK 的話才輸出
System.out.println(responseString);
} else {
System.out.println(response.getStatusLine());
}
}
}
更多的程式範例可以參考其教學: 
HttpClient 4 HTTP request 
HttpClient 4 使用POST方式提交普通表單數據的例子

關鍵字:Apache、HttpClient、4、教學、示範、範例、設定
參考資料:
  1. Apache HttpClient 首頁
  2. HttpComponents HttpClient 4.0 (GA)
  3. Apache HttpClient Tutorial
  4. Apache HttpClient apidocs
  5. HttpClient 4 HTTP request
  6. HttpClient 4 使用POST方式提交普通表單數據的例子 

2010年11月16日 星期二

[轉載] JSON傳送與接收


原文

XML可用來表現階層性資料,然而建立與處理XML DOM較為複雜
在Web應用程式中,許多時候並不需要用到XML的複雜階層性
此時通常會採用JSON作為資料交換格式
而JSON也正成為Web應用程式中交換資料的主要選擇。

JSON全名JavaScript Object Notation,為彷造JavaScript的物件實字(Object literal)格式而來
你可以在 http://www.json.org/ 找到詳細的JSON格式說明
大致而言,與物件實字格式類似,主要注意的是JSON:
作為名稱的部份,必須用"雙引號包括
作為值的部份,若為字串,必須用"雙引號包括
不支援函式表示

舉個例子來說,下面是個物件實字:
var obj = {
    name : 'Justin',
    age : 35,
    childs : [ { name : 'hamimi', age : 3} , { name : 'another', age : 5}]
};

若使用JSON表示,則是如下:
var json = '{"name":"Justin","age":35,"childs":[{"name":"hamimi","age":3}, { "name" : "another", "age" : 5}]}';

若為排版會比較容易觀察:
{
    "name":"Justin",
    "age":35,
    "childs":[
        {
            "name" : "hamimi",
            "age" : 3
        },

       {
            "name" : "another",
            "age" : 5
        }

    ]
}

你可以傳送JSON字串給伺服端,要建立JSON字串很簡單
在Firefox 3.1、Internet Explorer 8以上,支援簡單的JSON處理程式庫
如果要從JavaScript建立JSON字串,只要使用JSON.stringify(),例如:
var obj = {
    name : 'Justin',
    age : 35,
    childs : [ { name : 'hamimi', age : 3}, { "name" : "another", "age" : 5} ]
};
var json = JSON.stringify(obj);

這樣就可以得到方才所示範的JSON字串,如果要用非同步物件傳送JSON
則可以如下:
var request = xhr();     // xhr() 會建立非同步物件
request.onreadystatechange = handleStateChange;  // handleStateChange 參考至函式
request.open('POST', url);
request.setRequestHeader('Content-Type', 'application/json');
request.send(json);

請求標頭'Content-Typ'建議設為'application/json'
當然,伺服端要能夠剖析JSON字串以取出資料,但無需親自撰寫
http://www.json.org/ 網站中提供許多語言實作的JSON剖析器協助剖析JSON取得結果

伺服端可以傳回JSON字串
可以使用eval()將JSON字串計值(evaluate)為JavaScript物件
例如:
var obj = eval(json);

然而eval()也會運算傳入的JavaScript程式碼,因此並不建議直接用eval()
如果只是要計值JSON為JavaScript物件,可以使用JSON.parse()。例如:
var obj = JSON.parse(json);

如果是Firefox 3.1、Internet Explorer 8以外沒有內建JSON支援的瀏覽器
可以在 http://www.json.org/ 下載 json2.js
將之包括在網頁中,即可獲得上述的相關JSON方法:
<script type="text/javascript" src="json2.js">
如果遇到瀏覽器已內建JSON支援,json2.js什麼都不作

2010年11月12日 星期五

[轉載] javascript :onsubmit=return false阻止form表單提交

return false 阻止表單提交不起今天這個問題困擾了我很久,在網上找了很多資料,基本上關於onsubmit=return false有以下幾點要注意的地方:

1. return 的返回值問題,函數中return一旦有返回值,就不在執行下面的語句,直接跳到函數調用 的地方。如下PHP函數代碼,第一個if條件符合則函數值返回布爾型false,可以返回一個函數的值,並且跳出這個函數;只要遇到return語句,程序就在那一行代碼停止執行,執行控制將立刻返回到調用該程序的代碼處。
 function chkinput(form)
 {
   if(form.title.value=="")
    {
      alert("請輸入文章標題!");
   form.title.select();
   return false;              //注意不能寫成 return(false);
    }

   if(form.content.value=="")
    {
      alert("文章正文不能為空@!!");
   form.content.select();
   return false;
    }

    return true;
 }

2.form的onsubmit屬性的觸發問題,onsubmit 事件什麼時候觸發?onsubmit 事件會在表單中的確認按鈕被點擊時發生。不觸發的原因有一般如下:
A. onsubmit屬性的觸發時機是在form用input:submit這樣的button提交時才會觸發,否則不會觸發。如果是用一個普通input:button,則在onclick屬性中指定一個javascript函數,在這個函數裡面再執行form的submit()函數,而不是onsubmit屬性。
B. 先看一段代碼:
<form action="index.jsp" method="post" onsubmit="submitTest();">

<INPUT value="www">

<input type="submit" value="submit">

</form>
<SCRIPT LANGUAGE="JavaScript">

<!--

function submitTest() {

// 一些邏輯判斷return false;

}

//--></SCRIPT>
點擊submit按鈕該表單並未提交。因為有一處應該改為(紅色字體)
<form action="index.jsp" method="post" onsubmit="return submitTest();">
原來onsubmit屬性就像是<form>這個html對象的一個方法名,其值(一字符串)就是其方法體,默認返回true;
和Java一樣,在該方法體中你可以寫任意多個語句,包括內置函數和自定義函數。
在這裡submitTest()雖然返回false,但我們只執行了此函數,沒有對其結果進行任何處理。
onsubmit="return submitTest()利用到了它的返回值,達到了預期效果。
 
3.事件處理函數返回false的問題,在大多數情況下,為事件處理函數返回false,可以防止默認的事件行為.
例如,默認情況下點擊一個<a>元素,頁面會跳轉到該元素href屬性指定的頁.   
Return False 就相當於終止符,Return True 就相當於執行符。  
在js中return false的作用一般是用來取消默認動作的。比如你單擊一個鏈接除了觸發你的  
onclick時間(如果你指定的話)以外還要觸發一個默認事件就是執行頁面的跳轉。所以如果  
你想取消對象的默認動作就可以return false。return false應用比較多的場合有:  

<body>  
1, <a href="a.JSP" mce_href="a.JSP" onclick=test();>超級鏈接 </a>  
2, <input type="button" onclick=test() value="提交">  
3, <form name="form1" onsubmIT="return test();">  
內容  
<input type="submIT" value="提交">  
</form>  
</body>  

    
<input type="submit" onclick="submitAction(); return false;" />  
submitAction 方法裡面有提交表單的動作。如果不加 return false,
在執行完 submitAction 之後,submit 按鈕還會繼續執行它的默認事件,
就會再次提交表單。這可能就是很多錯誤的根源。   
的確,return false的含義不是阻止事件繼續向頂層元素傳播,
而是阻止瀏覽器對事件的默認處理。你可以這樣試驗:
首先將所有的js腳本註釋掉,在IE瀏覽器中嘗試拖動一下圖片,
你會發現鼠標會成為禁止操作的樣式,圖片是被禁止拖動的,
它是瀏覽器針對mousemove事件所提供的默認行為。
return false就是為了去掉這種行為,否則就會出現你描述的中斷事件連續執行。   
另外,和return false等效的語句為:
window.event.returnValue = false,
你可以把return false替換為此語句並進行驗證。   
最後說明一下,此種方式只適用於IE瀏覽器。
在js中return false的作用一般是用來取消默認動作的。比如你單擊一個鏈接除了觸發你的 
onclick時間(如果你指定的話)以外還要觸發一個默認事件就是執行頁面的跳轉。所以如果
你想取消對象的默認動作就可以return false。return false應用比較多的場合有:
<form name="form1"  onsubmit="return youfunction();">...... </form>
<a href="www.***.com" onclick="...;return false;">dddd </a>

2010年11月7日 星期日

自訂編碼標籤範例

如果不將Eclipse的環境編碼改成UTF-8,似乎轉碼會有問題
因應這種情況寫了自訂標籤來協助處理

encrypt.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">

    <tlib-version>1.2</tlib-version>            
    <short-name>encryptlib</short-name>    
    <uri>encrypt-tags</uri>                              
    <tag>
      <description>URL  get/post encrypting</description>
      <name>encrypt</name>
      <tag-class>tw.vencs.EncrptTag</tag-class>
      <body-content>empty</body-content>
    
      <attribute>
          <name>encryptValue</name>
          <required>true</required>
          <rtexprvalue>true</rtexprvalue>
      </attribute>
    </tag>

</taglib>

EncrptTag.java===============

package tw.vencs;

import javax.servlet.jsp.tagext.SimpleTagSupport;
import javax.servlet.jsp.JspException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class EncrptTag extends SimpleTagSupport{
    private String encryptValue; 
   
    public void doTag() throws JspException { 
        try { 
            encryptValue = new String(encryptValue.getBytes("iso-8859-1"),"UTF-8"); 
            getJspContext().getOut().write(encryptValue);
        } catch (IOException e) { 
              Logger.getLogger(EncrptTag.class.getName()).log(Level.SEVERE, null, e);
              throw new RuntimeException();
        }    
    } 
    public void setEncryptValue(String encryptValue) { 
        this.encryptValue = encryptValue; 
    } 
}

Demo.jsp===============

.....
<%@ taglib prefix="e" uri="encrypt-tags" %>
.....
<body>
.....
....=<e:encrypt  encryptValue="${param.itemName}"/>
</body>
.....

2010年11月5日 星期五

[轉錄]Tomcat Post / Get 中文編碼處理方法 (中文亂碼問題)

最近在把原本佈署在 Resin 的 Java Web Application移植到 Tomcat 時,有許多原本正常的中文get / post 功能,都變的異常,只要是透過 get / post 得到的資料,就會變亂碼
剛好也驗證了 Resin 在Character Encoding的部分做的較好
而看看網路上大家的問題,也可以知道 Tomcat 在編碼處理的部分 真是讓很多人頭痛

主要的原因是,TOMCAT 預設都是用 ISO-8859-1 的編碼方式來傳遞資訊
這個問題,可以解決的方式整理如下:

1. JSP 頁面的編碼宣告需與實際儲存檔案時用的編碼一致

這是個很好玩的問題~ 有些人 在頁面宣告用 big5 or UTF-8 or... 字集
但是,檔案實際儲存的方式與該編碼不同,則 頁面當然會出現亂碼的問題

2. 自行轉碼
form method=POST or GET 時 因為Tomcat預設編碼是ISO-8859-1的關係
直接用 <%=request.getParameter("firstname")%> 取值,中文字會變亂碼
(你傳的明明是UTF-8, 但是讀取時 確用 ISO-8859-1來解譯.. 當然有問題囉)
要使用 <%=new String(request.getParameter("firstname").getBytes( "ISO-8859-1"), "UTF-8")%>這種方式來轉換編碼才會正常 (假設,頁面用的編碼為UTF-8時)

3. 透過設定 server.xml URIEncoding的方式 來處理 GET 亂碼問題
在 tomcat 的設定檔內 ./conf/server.xml 修改下列設定的話,則透過GET的方式傳遞參數
的話,Tomcat會用你指定的編碼方式來對待用get方法傳遞的參數,而不是使用預設的ISO-8859-1 (get 的方式是透過 url parameter傳遞參數)
但是,因為只處理 URIEncoding, 所以若使用 Post 傳遞,還是會有問題
URIEncoding: This specifies the character encoding used to decode the URI bytes, after %xx decoding the URL. If not specified, ISO-8859-1 will be used.

P.S. 若使用 GET Method, 且設定了URIEncoding 的話就不用再做上述二的自行轉碼動作,如果多加該處理,一樣會有亂碼的問題

P.S.2  如果整合apache和Tomcar的話,需要將兩者溝通用的8009 port也設置URIEncoding

4. 使用 request.setCharacterEncoding 來處理 GET / POST 的亂碼問題
在處理的頁面,加上下列的處理即可,明確的告訴Tomcat request的編碼為何,不要一廂情願的使用ISO-8859-1來解讀~

<%request.setCharacterEncoding("UTF-8");%>

javax.servlet.servletRequest.setCharacterEncoding(string env)
Overrides the name of the character encoding used in the body of this request. This method must be called prior to reading request parameters or reading input using getReader(). Otherwise, it has no effect.

5. 使用 filter 來處理 GET / POST 的亂碼問題
這個方法是最方便的,只要在 web.xml 裡面 透過 filter 的設定,讓每個網頁都能
透過 filter 的處理一體適用所有編碼問題的解決
方法如下:

a. 目前 Tomcat 提供的範例裡面,就有現成的 Encoding Filter, 位置如下:
.\Tomcat 6.0\webapps\examples\WEB-INF\classes\filters\SetCharacterEncodingFilter.class

b. 所以,只要把該 class 檔,copy 到你的 web application 的
.\WEB-INF\classes\filter 下,再搭配 web.xml 的filter 設定,
就可以讓所有符合 url pattern 的網頁,都能自動apply所想要的編碼設定(透過 encoding這個 parameter來設定),是不是很方便呢 ?
(這個功能 就像是自動把上述第四點的 setCharacterEncoding 語句 自動apply到各頁面囉)

------------
以上~ 就從 Resin Porting 到 Tomcat 的經驗,當然是使用 filter 的方式 一次解決~ 不然的話 就得每個網頁每個網頁去改囉~

延伸資料:
FAQ/CharacterEncoding
http://jim.blogsome.com/2005/05/27/jsp-chinese-character-solution/
http://www.javaworld.com.tw/confluence/pages/viewpage.action?pageId=752

2010年10月21日 星期四

Ajax 入門簡介

網頁程式碼常被劃分為  - HTML程式碼、儲存資料的XML、處理資料的JavaScript
動態資料儲存在XML,可以達成在不動到HTML碼的前提下修改網頁內容
這樣的快速資料更新與動態呈現的技術就是Ajax
用戶端開始Ajax請求,並等待伺服器的回應
伺服器收到請求後回傳資料,並由JavaScript居中媒介及整合資料到網頁裡

在這段過程中
使用者依然可以持續享受原本的服務,並等待在背景執行的Ajax請求執行結束
這也就是Ajax名稱的由來(Asynchronous JavaScript + XML)

JavaScript內建一個XMLHttpRequest物件
用於初始Ajax請求,並處理Ajax回應
它有比較複雜的類別成員及方法:
abort() -
取消Ajax請求

open() -
準備請求,指定請求的型態、URL與其他細節

send() -
傳送請求給伺服器

readyState -
請求的狀態碼代號;0表示未初始,1是開啟,2是已傳送,3是接收中,4是已載入

status -
HTTP的請求狀態代碼(ex:404錯誤,200表示正常)

onreadystatechange -
請求狀態改變時會被呼叫的函式參考
                              
responseText -
伺服器回傳的純文字字串資料

responseXML -
伺服器回傳的的XML資料


因為不同的瀏覽器(其實是IE問題比較大...)有不同的 XMLHttpRequest 物件建立方式
建立時必須先寫如下的程式碼:
var request = null;
if(window.XMLHttpRequest){
    try{
      request = new XMLHttpRequest();   
    }catch(e){
      request = null;
    }
}
//for IE version
else if(window.ActiveXObject){
    try{
      request = new ActiveXObject("Msxm12.XMLHTTP");   
    }catch(e){
      try{
        //for older versions of IE 
        request = new ActiveXObject("Microsoft.XMLHTTP");         
      }catch(e){
        request = null;     
      }
    }
}

部分版本的 Mozilla 瀏覽器,在伺服器送回的資料未含 XML mime-type header 時會出錯
解決方法是,用下列方法覆寫伺服器傳回的檔頭,以免傳回的不是 text/xml:
request = new XMLHttpRequest();
request.overrideMimeType('text/xml');

建立Ajax請求物件後,接著要設定伺服器傳回資料後的處理方式,之後再開啟請求
API 如下:
request.onreadystatechange = handler;       //handler是處理傳回值的 JavaScript 函式名稱
request.open(type, url, true);                          //true set for being always asynchronous

onreadystatechange 直接指向 handler() 的位置
這裡用匿名函式來處理也是可以的
request.onreadystatechange = function(){
    //code
}

open() 的第一個參數是 HTTP request 的方法
也就是從 GET、POST、HEAD 等伺服器支援的方法中擇一使用
為遵循 HTTP 標準,請記得這些方法都是大寫
否則有的瀏覽器不會處理這些請求
第二個參數是請求頁面的 URL
基於安全考量,呼叫同網域以外的網頁是被禁止的
如果網域不同,則叫用 open() 時會出現「權限不足,拒絕存取」那類的錯誤
常見的錯誤多為在 domain.tld 的網站下呼叫 www.domain.tld 中的網頁
僅是一點點差別都不行
第三個參數決定此 request 是否不同步進行
如果設定為 TRUE 則即使伺服器尚未傳回資料也會繼續執行其餘的程式

type一般選擇填入 "GET" 或 "POST"
GET 適用於不會改變伺服器任何設定的資料傳輸
有需要的話還能透過 url 傳小量資料到伺服器
範例:
request.open("GET", "Data.xml", true);
request.send(null);                                         //請求傳送時沒有資料,所以設定成null  

POST 適用於會變更伺服器設定的資料傳輸
範例:
request.open("POST", "addData.php", true);
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;
                                                 charset=UTF-8"); 
request.send("date=10/10/18&demo_string=Hello World!");     //資料附在send()的參數裡傳送

send() 的參數在以 POST 發出 request 時,可以是任何想傳給伺服器的東西
不過以 POST 方式傳送資料前,必須先設定好 MIME type 才能讓伺服器理解資料的處理方式
application/x-www-form-urlencoded; charset=UTF-8 表示資料的型別是 URL-encoding
而資料則以查詢字串的方式列出


Mozilla教學的範例:
test.xml===========

<?xml version="1.0" ?>
<root>
I'm a test.
</root>

test.html===========

<script type="text/javascript" language="javascript">
function makeRequest(url) {
    var httpRequest;

    if (window.XMLHttpRequest) { // Mozilla, Safari, ...
        httpRequest = new XMLHttpRequest();
        if (httpRequest.overrideMimeType) {
            httpRequest.overrideMimeType('text/xml');
        }
    }
    else if (window.ActiveXObject) { // IE
        try {
            httpRequest = new ActiveXObject("Msxml2.XMLHTTP");
        }
        catch (e) {
            try {
                httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch (e) {}
        }
    }

    if (!httpRequest) {
        alert('Giving up :( Cannot create an XMLHTTP instance');
        return false;
    }
    httpRequest.onreadystatechange = function() { alertContents(httpRequest); };
    httpRequest.open('GET', url, true);
    httpRequest.send('');

}

function alertContents(httpRequest) {

    if (httpRequest.readyState == 4) {         
        if (httpRequest.status == 200) {                //表示已傳輸完成且正常
            var xmldoc = httpRequest.responseXML;
            var root_node = xmldoc.getElementsByTagName('root').item(0);
            alert(root_node.firstChild.data);
        } else {
            alert('There was a problem with the request.');
        }
    }

}
</script>

<span
style="cursor: pointer; text-decoration: underline"
onclick="makeRequest('test.xml')">
    Make a request
</span>

2010年10月17日 星期日

JavaScript 筆記(3)

這篇筆記紀錄:
1.JavaScript的物件導向寫法與型別判斷
2.陣列的排序法
3.以for迴圈及foreach迴圈取出物件的差別
4.String物件的常用方法
5.Math 物件的常用方法
6.addEventListener 與 attachEvent 函式說明

1.JavaScript的物件導向寫法與型別判斷

   JavaScript本身就是個以物件為主體的script語言
   連函式本身也是物件,或者說JavaScript的function類似其他語言的class

   除了數值、字串、boolean、null、undefined,其他的所有值都是物件

   typeof運算子在判斷特性的型別有很大的幫助
   typeof obj.num         // 'number'
   typeof obj.arrival      // 'object'

   // 要小心有可能是涵式的值,因為原型鍊上的特性都可以產生值
   typeof obj.toString            // 'function'
   typeof obj.constructor      // 'function'

   hasOwnProperty() 在物件有指定屬性時回傳true,這個方法不會檢索原型鍊
   可以用來避免上述風險
   obj.hasOwnProperty('number')        //true
   obj.hasOwnProperty('constructor')  //false

   當需要自訂物件時,必須寫好物件的建構式,並使用new關鍵字來產生物件
   而JavaScript的類別成員不需要先宣告也不用處理型別,直接用this關鍵字來取得值
   範例:
    function ObjDemo(param1, param2){      //建構式必須首字大寫
        this.param1 = param1;
        this.param2 = param2;

        this.output = function(){
          return param1 + " " + param2;
        }
        ObjDemo.prototype.output2 = function(){
          return "This is a test ";

        }
    }

    var test = new ObjDemo("word1", "word2");
    test.output();
    test.output2();

    範例的程式碼可看出有兩種類別方法的寫法
    第一種用 this 關鍵字的方法會將方法指派給單一實例
    每建立一個物件時,物件內都會有方法的複本,處理方式像類別成員
    建立多個物件就會產生等同物件數量的方法複本
    第二種寫法使用了物件裡以特性的形式存在的prototype物件
    如此可建立類別所擁有的方法,所有建立的物件都會使用同一個方法    
    這樣可以改進資源的使用效率

    JavaScript實際上並不完全支援類別,但用prototype物件來模擬類別
    利用它也可以做到將類別成員給予物件類別共用
    ObjDemo.prototype.param3 = "This effect is like static member of other languages";


2.陣列的排序法

   範例:
    var tarray = [11, 35, 12, 9, 29];
    tarray.sort();

    sort() 方法預設依照升冪順序排列
   如果希望的排列規則不同,或是陣列裡存放的物件不是基本型別的話(也就是自訂物件)
   則依下列範例的形式修改:
    function compare(x, y){      //傳入物件x、y
      return x.value - y.value;     //回傳值<0,x排在y前;相同時則不排列;>0,y排在x前
    }
    tarray.sort(compare);

   又因為通常排序的函式只會用於sort() method
   所以寫成 tarray.sort( function(x, y){return x.value - y.value;} );  也行
   這樣也不需要為函式命名

3.以for迴圈及foreach迴圈取出物件的差別

   for ... in ... 敘述為JavaScript中for each的語法,但是為隨意排序取出資料;
   若想確保資料有一致的順序還是要乖乖用for迴圈

4.String物件的常用方法

    indexOf() 尋找字串是否包含特定子字串,找不到特定子字串則回傳 -1   
    charAt() 尋找特定字元在字串裡的位置
    toLowerCase()、toUpperCase()則是轉換字串大小寫的方法
    length 成員變數則提供字串裡的字元數量的訊息

    字串雖然看起來很像陣列物件,但沒辦法用 [index] 的形式去讀取它

5.Math 物件的常用方法

    Math 物件提供了常用的數學方法
    random() 產生介於0和1之間的亂數
    round() 將浮點數四捨五入為整數
    floor() 將浮點數無條件捨去為整數
    ceil() 將浮點數無條件進位為整數
    abs() 回傳絕對值
    PI 是Math物件提供的常數,值為3.14

6.addEventListener 與 attachEvent 函式說明

   addEventListener 是DOM的標準功能之一,能為某一事件附加其它的處理事件
   一般常見到的指定事件的處理函式的寫法如:
   window.onload = method1;       //指定處理的函式是method1()

   或是用匿名函式來處理:
   window.onload = function(){ method1(); }

   第一種方法的缺點在於一個事件只能定義一種處理,如:

   document.getElementById("btn").onclick = method1;
   document.getElementById("btn").onclick = method2;
   document.getElementById("btn").onclick = method3;    //只有最後定義的method3()會被執行

   addEventListener 是為了解決這個問題存在的
   函式為 addEventListener(event,function,capture/bubble)
   第一個參數是參數接收事件名稱(名稱不含on,如:"load" or "click")
   第二個參數是要執行的函式
   第三個參數設定 capture 或 bubble 的事件讀取方式,以bool值表示
   簡單來說 capture 從讀取完document後再執行事件
   而bubble則是先尋找指定的網頁元件位置再執行事件
   true 表示選擇 capture,false 指定用 bubble,一般指定 false 即可

   IE不支援 addEventListener,要改用 attachEvent 這個IE特有的函式
   傳入的參數大致上相同,只是第一個參數要改成完整事件名稱(如:"onclick")
   而因為IE的事件的傳遞讀取方式預設用 bubble,所以第三個參數可以省略不輸入

   完整範例:
    var bt = document.getElementById("bt");

    if(window.addEventListener){
       bt.addEventListener("click",method1,false);
       bt.addEventListener("click",method2,false);
       bt.addEventListener("click",method3,false);
    }else if(window.attachEvent){
       bt.attachEvent("onclick",method1);
       bt.attachEvent("onclick",method2);
       bt.attachEvent("onclick",method3);
   }

   執行順序為method1->method2->method3

JavaScript DOM模型操作

Document Object Model(DOM) 能將HTML視為XML處理
對於JavaScript來說,可以藉此做到動態處理
DOM將網頁視為節點構成的階層樹,如下圖所示:

階層樹的頂點是Document,並往下聯繫到各個網頁元件形成的節點
HTML元件節點也被稱為元件節點(Element node)
網頁元件的文字內容則儲存在元件下的文字節點(Text node)
Attribute是元件的屬性,可以透過Element node取得,但不會直接出現在階層樹裡

document 提供了 getElementById()、getElementsByTagName()方法來導引到其它子節點

getElementById() 可以從整份DOCUMENT中取得有指定id的元素
getElementsByName() 與 getElementsByTagName() 也是有著類似作法的函式

不過因為後兩個函式取得的元件有可能為複數,所以需要用陣列存取
如:getElementsByTagName("div")[0]  取得第一個div元素

getElementById()一般是最常用到的函式
id在每個DOCUMENT中只能是獨特的,name卻有可能是重複的
此外, getElementsByName()在非IE的瀏覽器可能行不通


在各個節點可以取得我們需要的特性:
nodeName - 回傳節點的標籤名稱,如<div>會回傳 DIV
nodeValue - 儲存於節點的值,只限文字與屬性節點使用
nodeType - 回傳節點型態
                   element node回傳1,attribute node回傳2
                   text node回傳3,包括換行符號等
childNodes - 包含節點下所有子節點的陣列,以出現在HTML碼裡的順序排列
childElementCount - 回傳 element 子代 (child) 元素的數量
parentNode - 回傳其父節點
firstChild - 節點下的第一個子節點
lastChild - 節點下的最後一個子節點
nextSibling - 下一個節點(節點範圍不限同一層)
previousSibling - 上一個節點(節點範圍不限同一層)

較特殊的:
document.baseURI - 取得目前文件的路徑(即目前網址)
style - 設定或取得某個節點的屬性,類似CSS設定,如:node.style.width='100px';
className - 設定或取得節點的class屬性


另外說明.children屬性
這個也是相當常被看到的屬性,使用的方式也很類似childNodes
差別在於
1.childNodes是W3C標準,children則不是(但基本上browser都會實作這個屬性)
2.childNodes會回傳 NodeList,children則是HTMLCollection
3.children回傳的內容不包括 text node等,僅回傳 element node
   所以如有下列的情形
   <div>
         <a></a>
   </div>

   像這樣節點間存在換行符號、空白
   用 childNodes.firstChild 便有可能取不到我們真正想要的 node(視 browser 實作情形)

可以在節點上使用的方法則有
appendChild() - 加入子節點
cloneNode() -  參數設定為 true 可複製該元素的所有子代 (child)
                        設定為 false 則通常只會複製一個文字節點
    範例:
    var demo = document.getElementById("demo");
    var demo2 = demo.cloneNode(true);
    document.body.appendChild(demo2);

hasChildNodes() - 回傳boolean值,判斷有無子節點
hasAttributes() - 回傳是否有無屬性值
insertBefore() - 使用方式:parentNode.insertBefore(newElement, referenceElement)
removeChild() - 刪除子節點,使用方式:parentNode.removeChild(node)
replaceChild() - 使用方式:node.replaceChild(newChild, oldChild);
getAttribute() - 取得節點的某項屬性,如:node.getAttribute("nowrap")
setAttribute() - 設定節點的某項屬性,如:node.getAttribute("height", "100px")
removeAttribute() - 刪除節點的某項屬性,如:node.getAttribute("nowrap")


使用範例:document.getElementsByTagName("div")[0].childNodes[1].nodeValue = 1;
當然範例要先確定childNode[1]底下只有一個節點,否則會得不要想要的效果
如果不確定所要更改的節點內容是否有多個子節點
那麼更改的時候就要先清除所有的子節點後,再新增有新內容的新子節點,如:
var node = document.getElementById("target");
//有子節點的話,firstChild回傳true
while(node.firstChild){ node.removeChild(node.firstChild); }
node.appendChild(document.createTextNode("alter OK"));

當然先寫好一個替換節點文字內容的函式,通常是個方便的選擇,如:
function replaceNodeText(id, newText){
   var node = document.getElementById(id);
   while(node.firstChild){ node.removeChild(node.firstChild); } 
   node.appendChild(document.createTextNode(newText));
}

DOM提供了透過節點調整CSS的機制
透過節點物件的className特性可以操作網頁元件的類別(class)特性
如:document.getElementById("target").className = "deco2";
節點的style特性提供對單一樣式性質的存取
如:document.getElementById("target").style.visibility = "hidden";

前面看到過的createTextNode()只能建立純粹的文字內容
接著要提的createElement() 可以用來建立任何的HTML元件
建立段落文字的範例:
var newElem = document.createElement("p");
newElem.appendChild(document.createTextNode("Hello World"));
document.getElementById("target").appendChild(newElem); 

加入的段落文字會以符合標準的形式呈現 -> <p>Hello World</p>