2008年10月24日 星期五

Google JMesa、Flying Saucer、iText 的中文問題

Google JMesa 是一個功能蠻強大的 HTML Data Grid,不只提供分頁、篩選等功能,還可以將結果匯出成 Excel 或 PDF 檔案。之前上課的時候我在 Lab 裡面用到,不過因為處理的都是英文資料,所以看不出問題,這兩天有位學員問我中文亂碼怎麼處理,我才意識到有這個問題。

以 JMesa 本身提供的範例來說,我看了一下 WEB-INF\lib,看到 iText 的 JAR 檔案,我本來以為只要按照調整 iText 的方式就可以成功,所以就放心了。結果我 Trace了一下Source Code:

1. BasicPresidentController 會呼叫 TableFacade 的 render 方法
2. TableFacade 的 render 方法會長個 PdfViewExporter,呼叫 export 方法
3. PdfViewExporter 的 export 方法會長個 org.xhtmlrenderer.pdf.ITextRenderer,呼叫 createPDF 方法

這時問題就來了,JMesa 沒有直接用 iText,將結果輸出為 PDF,這樣要處理的細節比較多,比較累。因為 JMesa 可以產生 HTML,透過 CSS 與 JavaScript 做出其他功能,所以它就利用 Flying Saucer 這個封裝 iText 的 HTML/XHTML Parser,也就是上面看到的 xhtmlrenderer 相關套件,把畫面上看到的 HTML+CSS 透過 Flying Saucer 底層封裝的 iText 去產生 PDF:

JMesa -> HTML+CSS -> Flying Saucer -> iText -> PDF

所以,我們不能直接用修改 iText 的方式去解決中文,而要用 Flying Saucer 的方式去解決,不過底層一樣是 iText,所以其實改法大同小異,網路上搜尋一下,可以找到底下的做法

import com.lowagie.text.pdf.BaseFont;

...

ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont(
"C:/WINDOWS/Fonts/中文字型名稱.TTF",
BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED 或 BaseFont.EMBEDDED
);

不過呢,這個做法不 Work。

解決這個問題,要從上面箭頭的流程由右往左一步一步解決:

iText -> PDF

iText 要能夠產生中文 PDF,第一要有它可以處理的中文字型,目前最常用的 Unicode 編碼TrueType 字型不見得可以順利載入,因為 iText 對 TrueType Collection 與 OpenType 支援的程度似乎還沒有很好,所以要先找出 iText 可以用的中文字型,免得明明一切步驟都對,就敗在字型這一關。幾乎每台裝了 Office 的 Windows 電腦都會有 Arial Unicode MS (arialuni.ttf) 這個字型,iText 也認得,所以我就以這個字型做示範。

第二步,要試出正確的編碼。有些中文字型可以用 BaseFont.IDENTITY_H,有些可以用 UniCNS-UCS2-H,要試過才知道。Arial Unicode MS 用的是 BaseFont.IDENTITY_H,但是像 Adobe Reader 附贈的中黑體就要用 UniCNS-UCS2-H。

第三步,要有 iTextAsian.jar 檔案,跟 iText 的 JAR 檔案放在一起。Flying Saucer 跟 JMesa 內含的 iText 版本比較舊,目前最新是 2.1.x 版,但是跟 Flying Saucer 搭配會有問題,因為 Flying Saucer 會用到新版 iText 已經不提供的 API。所以,我把 iText 的 JAR 檔案換成 2.0.8 版,2.1 之後的版本就不行了。

Flying Saucer -> iText -> PDF

Flying Saucer 目前最新是 Release 8 – Second Code Drop (R8pre2) 版本,JMesa 內含的舊了一點,所以我也更新了。

我準備了一個簡單的 XHTML 檔案:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>中文測試</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
name
{
font-family: "Arial Unicode MS";
color: blue;
font-size: 48;
}
</style>
</head>

<body>
<name>名偵探小怪獸</name>
</body>

</html>

然後模仿網站上找到的範例,直接寫一個 Java 類別去產生 PDF 檔案:

import java.io.*;
import org.xhtmlrenderer.pdf.*;
import com.lowagie.text.pdf.*;

public class Test
{
public static void main(String[] args)
{
try
{
String inputFile = "test.xhtml";
String url = new File(inputFile).toURI().toURL().toString();
String outputFile = "test.pdf";
OutputStream os = new FileOutputStream(outputFile);
ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont(
"C:/Windows/Fonts/arialuni.ttf",
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED);
renderer.setDocument(url);
renderer.layout();
renderer.createPDF(os);
os.close();
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}

這樣就成功了:

image

還可以看到字型的資訊:

image

到目前為止,跟網路上找到的解法,差別如下:


  1. 網路上只說明 Java 程式怎麼加入中文字型,可是一般卻沒有說明必須透過 CSS 去指定使用這個字型,所以雖然加入字型,但是卻沒有被使用到,所以還是會一直看到中文亂碼。

  2. 網路上沒有提醒要加入 iTextAsian.jar 檔案。

另外,其實除了 addFont 方法之外,還有 addFontDirectory 方法,可以一口氣把某個目錄所有字型都註冊。不過這個方法只要一碰到問題就執行不下去,比方說要嵌入不准嵌入的字型,所以不建議貿然使用。

HTML+CSS -> Flying Saucer -> iText -> PDF

接下來,要解決 JMesa 匯出 PDF 會產生亂碼的問題。既然 Flying Saucer 已經可以順利產生中文 PDF,JMesa 也可以在 Browser 畫面上看到中文,那我就先把網頁畫面另存新檔,改一下剛剛的 Java 類別,看看能不能 Work。

網頁內容如下,重點是加入 css 目錄內的 jmesa-pdf.css 這個檔案,因為那是 JMesa 餵給 Flying Saucer 產生 PDF 要使用的 CSS:

<html>

<head>
<link rel="stylesheet" type="text/css" href="jmesa-pdf.css"></link>
<title>JMesa</title>
</head>

<body>

...

<tr id="basic_row1" class="odd"
onmouseover="this.className='highlight'"
onmouseout="this.className='odd'" >
<td><a href="http://www.whitehouse.gov/history/presidents/">喬治</a></td>
<td>Washington</td>
<td>1789-1797</td>
<td>Soldier, Planter</td>
<td>02/1732</td>
</tr>

...

</body>

</html>

記得 iText -> PDF 要注意的三件事,也不要忘了 Java 程式裡面註冊的字型,HTML+CSS 裡面必須要用到。所以我們修改一下 jmesa-pdf.css 檔案裡面顯示 Table 內容的設定:

.jmesa .odd td, .jmesa .even td {
/* font-family: verdana, arial, helvetica, sans-serif; */
font-family: "Arial Unicode MS";
/* font-size: 11px; */
font-size: 16;
padding: 2px 3px 2px 3px;
}

這麼一來,我們就可以將 JMesa 顯示的中文畫面手動地拿給 Flying Saucer 產生中文 PDF。做這件事的目的,是要確定 JMesa 產生的 HTML+CSS 究竟是否正確,不要改了半天,才發現源頭根本就出了問題。

image

JMesa -> HTML+CSS -> Flying Saucer -> iText -> PDF

最後一步,就是要想辦法把前一步自動化,這部份花了我最多時間。

首先,因為這一步要改一下 JMesa 的 Source Code,所以請解開 JMesa 的 Source Code,跟 JMesa 的範例程式放在一起。又因為要重新 Build 出 JMesa,也要更新 iText 與 Flying Saucer,所以請下載以下的 Library:


  1. iText 2.0.8:原因如上

  2. iTextAsian.jar:原因如上

  3. Flying Saucer R8pre2:原因如上

  4. JMesa 2.3.4:JMesa 範例裡面放的 JMesa JAR 檔案,請刪除,因為我們要修改它的 Source

  5. Spring Framework 2.5.5:換掉範例裡面 2.0.2 版的 Spring 與 AOP 模組

  6. Struts 2.0.12:要用到 XWork 的 JAR 檔案

  7. Java Portlet API:我從 Apache Pluto 裡面取得

上面的 5、6、7 三點可以不做,只是要換就換的徹底一點。 WEB-INF/lib 目錄下完整的檔案列表如下:

antlr-2.7.6.jar
asm-2.2.jar
cglib-nodep-2.1_3.jar
commons-beanutils-1.7.0.jar
commons-collections-3.0.jar
commons-dbcp-1.2.jar
commons-digester-1.5.jar
commons-io-1.3.jar
commons-lang-2.4.jar
commons-pool-1.2.jar
core-renderer.jar (新版的 Flying Saucer)
dom4j-1.6.1.jar
groovy-1.0.jar
hibernate-3.2.1.ga.jar
hsqldb-1.8.0.1.jar
iText-2.0.8.jar (新版的 iText)
iTextAsian.jar (新增)
jcl104-over-slf4j-1.4.3.jar
jexcel-2.6.6.jar
jstl-1.1.2.jar
jta-1.0.1B.jar
log4j-1.2.13.jar
minium.jar
poi-3.0-FINAL.jar
portlet-api-1.0.jar (新增,取自 Pluto)
servlet-api-2.4.jar
sitemesh-2.2.1.jar
slf4j-api-1.4.3.jar
slf4j-log4j12-1.4.3.jar
spring-test.jar (新增,取自新版的 Spring Framework,取代 spring-mock-1.2.6.jar)
spring-webmvc-portlet.jar (新增,取自新版的 Spring Framework)
spring-webmvc.jar (新增,取自新版的 Spring Framework)
spring.jar (新版的 Spring Framework)
standard-1.1.2.jar
tagsoup-1.1.3.jar
xercesImpl-2.6.1.jar
xwork-2.0.6.jar (新增,取自 Struts)

第二步,先能夠正確輸入與顯示中文資料。

修改 WEB-INF/presidents.txt 檔案,加入一些中文人名,這樣測試的時候就不必每次輸入中文,也可以順便測試中文顯示是否正確:

"喬治","Washington","Father of His Country", ...
"約翰","Adams","Atlas of Independence", ...
"Thomas","Jefferson","Man of the People, Sage of Monticello", ...
...

重新執行 JMesa 的範例,卻發現一樣顯示亂碼。這很簡單,應該是 JMesa 範例內的 JSP 檔案 Character Encoding 沒有設定 UTF-8,所以我就開始進行以下的動作。

修改 decorators/main.jsp 檔案,加入 UTF-8 設定:

<%@ taglib uri="sitemesh-decorator" prefix="decorator" %>
<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ...>

<html>

...

</html>

修改 jsp/*.jsp 檔案,加入 UTF-8 設定。以 basic.jsp 為例:

<%@ page contentType="text/html; charset=UTF-8" %>
<html>

<head>
<title>Basic JMesa Example</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>

...

</html>

改到這邊,我就很高興的重新執行,結果可以看到中文,太好了!

搞笑的是,輸入中文的時候可以看到中文,但是在 Worksheet 範例按下 Save 按鈕的時候,原本正確的中文卻會變成亂碼。這應該是 HttpServletRequest 物件 Character Encoding 沒有設定 UTF-8。

修改 org.jmesa.web.HttpServletRequestWebContext 類別的 Constructor,加入 UTF-8 設定:

public HttpServletRequestWebContext(HttpServletRequest request)
{
try
{
request.setCharacterEncoding("UTF-8");
}
catch (Exception e)
{
e.printStackTrace();
}

this.request = request;
}

到這個階段,JMesa 的範例才能夠正確輸入與顯示中文 (喬治與約翰是從檔案讀入的資料,湯瑪士是手動輸入的資料):

image

我本來以為這一步不是那麼重要,所以就先去改第三步,結果反而走了很多冤枉路。Jakarta Commons Lang 如果沒有正確設定 Character Encoding,StringUtils.escapeXml 方法就不能正確運作,會把正確的內容翻成亂碼。所以這一步一定要先改好才行!

第三步,能夠匯出中文 PDF 檔案。

修改 org.jmesa.view.pdf.PdfViewExporter 類別的 export 方法:

public void export() throws Exception
{
byte[] contents = view.getBytes();
responseHeaders(response);

...

ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont(
"C:/Windows/Fonts/arialuni.ttf",
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED
);

DocumentBuilder builder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
InputStream is = new ByteArrayInputStream(contents);
Document doc = builder.parse(is);

renderer.setDocument(doc, getBaseUrl());
renderer.layout();
renderer.createPDF(response.getOutputStream());
}

重新執行之後,不只可以正確輸入與顯示中文,在 Basic 範例按下匯出 PDF 的按鈕,還可以看到正確的中文 PDF 自動產生:

image

總算是大功告成。

不只是 PDF 檔案,連 Excel 都不會有問題喔:

image

2008年10月22日 星期三

LyX 與 W32TeX 的 XeTeX

網路上有很多先進說明了設定的方式,所以我只想針對 W32TeX 與 XeTeX 的部份記錄一些細節。

LyX 有好多種安裝檔案,我因為已經裝好 W32TeX,而且我不需要 DVI 與 PostScript 相關的支援,所以我下載的是最小的 LyX-1.5.6-1-Installer.exe。

安裝的時候,LyX 會詢問 TeX 執行檔案的位置,也就是 W32TeX 安裝位置下的 bin 目錄:

image

安裝之後,LyX 還是偷偷塞了Ghostscript 與 ImageMagick 進來。

接下來,要讓 LyX 透過 XeTeX/XeLaTeX 來產生 PDF 檔案,這部份可以修改 Home Directory 底下 Application Data\lyx15 目錄下的 preferences 檔案,或是 LyX 安裝位置下 Resources 目錄的 lyxrc.dist 檔案,加入底下這兩行:

\format "pdf4" "pdf" "PDF (xelatex)"  ""   ""   ""   "document,vector"
\converter "pdflatex" "pdf4" "xelatex $$i" "latex"
format 那一行會在 LyX 的 View 功能表多出一個 PDF (xelatex) 項目:

image

converter 那一行則會在 LyX 的 Tools、Preferences 畫面中 Converters 相關設定裡頭,多出一個 LaTeX (pdflatex) -> PDF (xelatex) 項目:

image

最後,在 LyX 的 Document、Settings 設定中,Language 裡面的 Encoding 選取 utf8-plain:

image

LaTeX Preamble 至少要寫底下這幾行,中文字型名稱可以更改:

\usepackage{fontspec}
\usepackage{xunicode}
\usepackage{xltxtra}

\setmainfont{PMingLiU} % 新細明體
\setmonofont{MingLiU} % 細明體

\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt

image

最後,只要確定 LyX 檔案是 UTF-8 編碼,那就可以了!

感謝以下兩位先進所提供的資訊:

1. http://bbs.ctex.org/viewthread.php?tid=41004

2. http://blog.bs2.to/post/EdwardLee/8545

2008年10月21日 星期二

Tomcat 與 HTTP Proxy

公司裡面所有的 Web Access 都必須透過 Proxy Server,一般只要在 Browser 裡面設定就好。

如果是 Java 程式自己要透過網路取得資料呢?請在執行時設定:

java -Dhttp.proxyHost=ServerName -Dhttp.proxyPort=ServerPort MainClass
如果是 Web 應用程式呢?以 Tomcat 為例,請修改 catalina.bat 的 JAVA_OPTS 參數,大概是在 118 行左右:

set JAVA_OPTS=%JAVA_OPTS%
-Dhttp.proxyHost=ServerName
-Dhttp.proxyPort=ServerPort
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file="%CATALINA_BASE%\conf\logging.properties"

ServerName 請換成 Proxy Server 名稱,ServerPort 請改為實際的 Port No。

MiKTeX vs. W32TeX

我的 S31 不知道為了什麼緣故,居然沒辦法裝 MiKTeX,每次裝到最後就掛掉。最新的 2.7 版是這樣,上一代的 2.6 版也是這樣。好不容易找到一個比較舊的 2.6 版安裝成功,可是沒有我最近在用的 XeTeX。上網找了一下,發現 W32TeXXeTeX,而且更新速度很快。用最小安裝,再加上幾個 PDF 相關的套件,以及 XeTeX,居然只要 100 MB 多一點,真是太酷了!

W32TeX 雖然日本人比較愛用,但是不管是處理英文或中文,完全都沒有問題喔!更方便的是,它的安裝過程其實只是解壓縮幾個檔案,完全不會動到 Windows 的 Registry,所以裝好一台機器之後,只要把目錄 ZIP 下來,就可以變成綠色軟體到處使用了!

IBM S31

最近把家裡的 IBM S31 拿出來整理,Pentium-III 600 MHz、256 MB RAM 的機器,即使是跑 XP,我還是覺得很慢。所以這一次,我刻意找出 Windows 2000 Professional,不加任何會讓機器變慢的 Service Pack,反正只是隨身攜帶、方便讀電子書的機器,偶爾做點筆記,上個小網,掛了就重灌。

裝好之後,上網找了相關的驅動程式,再加個 PCMCIA 無線網卡,只吃了 60 MB 的 RAM,跑起來比 XP 舒服多了。

接下來,該不該花點錢,買個 CF/SD 轉 IDE 的轉接卡,然後買塊 8/16/32G 的 CF/SD 卡,讓它跑起來更快呢?

2008年9月29日 星期一

REM - ZK Plugin for NetBeans

ZK 官方雖然只推出 ZK Studio 這個 Eclipse Plugin,不過 SourceForge.net 上有一個免費的 NetBean Plugin,名叫 REM,目前最新是 1.5.0 版。

裝好了之後,REM 會在 Web Application 中新增 ZK 這個 Framework,並且可以新增 ZUL、ZS、ZHTML 等檔案類型,撰寫 ZUL 檔案的時候也可以提供 Content Assist 功能,網站上提供的畫面如下,我就不另外抓了:


不過,目前好像還沒有提供 zscript 內 Java 程式碼的 Content Assist 功能。

使用 NetBeans 開發 Web Services

上課的時候,常常需要撰寫 Web Services,我個人覺得 NetBeans 這方面做的比 Eclipse 要來的好。

前兩天有個學員問我一個問題:如果有現成的 WSDL 檔案,可不可以像以前一樣產生 Server 端的 Skeleton?因為客戶會把需求寫成 WSDL,再交給他們去開發出 Web Services。

答案當然是可以的。

在 NetBeans 的專案上按下滑鼠右鍵,選取 New,就會看到三個跟 Web Services 有關的選項:
  1. Web Service...:這個是要從 Java 類別產生出 Web Service,當然也順便產生 WSDL
  2. Web Service Client...:這個是從 WSDL 產生出 Web Service 的 Client 端
  3. Web Service from WSDL...:這個就是從 WSDL 產生出 Web Service 的 Server 端,也就是所謂的 Skeleton
所以,不管是哪一種,都不需要像以前一樣寫一堆命令列參數。其實,當 NetBeans 產生專案時,就會為每個專案產生一個相對應的 build.xml 檔案,裡面就已經定義好一個一個 Ant Task,包括 Build、Build and Clean、Run,也包括撰寫或呼叫 Web Service 的相關 Task 了!

2008年9月18日 星期四

ZK 3.5.0 image 的 setContent 問題

ZK 可以用以下的方式顯示圖片:
<image id="photo" />
如果要清除圖片內容的話,可以用以下的方式:
photo.setContent(null); 
最近改用最新版的 ZK 3.5.0,上面的 會蹦出一個對話方塊,寫著 im == null。

問了 ZK 原廠,答案是:

舊版的 setContent 方法跟新版的 setContent 方法不太一樣:新版的 setContent 方法是一個 Overloaded 方法,有兩個版本:
public void setContent(org.zkoss.image.Image image)
public void setContent(java.awt.image.RenderedImage image)
所以要改用以下的寫法:
photo.setContent((org.zkoss.image.Image) null) 
可是我試了之後,還是一樣。後來看了一下 Javadoc 說明文件:
Calling this method implies setSrc(null).
In other words, the last invocatio of setContent(org.zkoss.image.Image)
overrides the previous setSrc(java.lang.String), if any.
我就把程式碼改了一下:
photo.setSrc(null); 
居然就恢復正常了!

2008年9月9日 星期二

JBoss AS 的 EJB 3.0 的命名衝突

JBoss AS 的 EJB 3.0 Support 一直做的都還不錯,所以上課的時候我很喜歡拿它來作示範。

前幾天發生一個狀況:程式明明沒錯,部署起來也沒看到什麼明顯的錯誤訊息,但是執行就是會有問題,一直抱怨找不到一些方法,可是我明明就有寫。

後來發現,我是直接部署 JAR 檔案,所以 Session Bean 的名稱預設就是 ClassName/remote 或 ClassName/local,因為我在兩個不同的 EJB Module 裡面定義了相同名稱、不同功能的 Session Bean,所以其中一個實際上並沒有部署成功。結果程式執行的時候,一直找到另一個功能不相同的同名 Session Bean,所以才會出問題。

解決方法是,要嘛把另一個有同名 Session Bean 的 EJB Module 移除,不然就是不要直接部署 EJB Module,先定義 Enterprise Application,加入 EJB Module 之後再部署,這樣 SessionBean 的預設命名方式就會變成 EARName/ClassName/remote 或 EARName/ClassName/local,也就不會產生命名衝突了!

2008年9月8日 星期一

GlassFish 的 DataSource

上課的時候,為了兼顧輕便與佔有率的考量,我喜歡用 MySQL 作為 Database Server。MySQL 預設的管理者帳號沒有密碼,一直以來使用上也沒出過什麼大問題。

這幾天有個課程需要用到 GlassFish,就在 GlassFish 裡頭定義了一個 MySQL 的 DataSource。沒想到,問題發生了:GlassFish 抱怨我的 DataSource 沒有設定 PasswordCredential,可是我的 MySQL 管理者帳號真的沒設定密碼啊!

試了半天之後,沒有什麼進展,所以我最後還是決定,幫我的 MySQL 管理者帳號設定密碼。然後,一切就搞定了!

2008年7月30日 星期三

Migration File Naming Scheme

2.1 版之前的 Rails,Migration File 命名方式採用 Numeric Naming Scheme:
001_xxx.rb
002_xxx.rb
...

2.1 版之後的 Rails,Migration File 命名方式採用 Timestamped Naming Scheme:
YYYYMMDDHHMMSS_xxx.rb
YYYYMMDDHHMMSS_xxx.rb
...

資料庫會多出一個新的 schema_migrations 表格,記錄目前執行過的 Migration。

如果不喜歡這種 Timestamp 方式,可以在 environment.rb 檔案設定:

config.active_record.timestamped_migrations = false

2008年7月14日 星期一

Notepad++

最近開始改用 Notepad++,也在 Port of Ruby Blue Theme 發現一個移植自 TextMate 的 Style,改過來之後發現還蠻好看的。

2008年7月2日 星期三

Spring Framework HibernateTemplate 的 initialize 方法

Spring Framework 提供了 HibernateTemplate,封裝 Hibernate Session 重要的 API,簡化程式的撰寫。

Session 的 load 與 get 有差異,HibernateTemplate 封裝的 load 與 get 當然就有差異,看 HibernateTemplate 的原始程式就很清楚:
public Object load(final Class entityClass, final Serializable id, final LockMode lockMode)
throws DataAccessException {

return executeWithNativeSession(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException {
if (lockMode != null) {
return session.load(entityClass, id, lockMode);
}
else {
return session.load(entityClass, id);
}
}
});
}

public Object get(final Class entityClass, final Serializable id, final LockMode lockMode)
throws DataAccessException {

return executeWithNativeSession(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException {
if (lockMode != null) {
return session.get(entityClass, id, lockMode);
}
else {
return session.get(entityClass, id);
}
}
});
}
可是有一件事很有趣,原本使用 Session/Transaction 這樣寫的程式碼:
try
{
tx = session.beginTransaction();
book = (Book) session.load(Book.class, bookID);
Hibernate.initialize(book);
tx.commit();
}
catch (Exception e)
{
if (tx != null) tx.rollback();
e.printStackTrace();
}

不能直接改成底下使用 HibernateTemplate 的寫法:
try
{
book = (Book) hibernateTemplate.load(Book.class, bookID);
hibernateTemplate.initialize(book);
}
catch (Exception e)
{
e.printStackTrace();
}

原因就是 HibernateTemplate 的 Javadoc 所說的:

This class can be considered as direct alternative to working with the raw Hibernate3 Session API (through SessionFactory.getCurrentSession()). The major advantage is its automatic conversion to DataAccessExceptions as well as its capability to fall back to 'auto-commit' style behavior when used outside of transactions. Note that HibernateTemplate will perform its own Session management, not participating in a custom Hibernate CurrentSessionContext unless you explicitly switch "allowCreate" to "false".

可是呢,即使把 allowCreate 設定為 false,還是會有問題:

Caused by: org.hibernate.HibernateException: load is not valid without active transaction

也就是沒有把 load 包在 Transaction 裡頭。

可是如果還要寫出 Transaction 的程式碼,那就麻煩了。

所以呢,就是透過 Spring Framework 提供的 AOP,掛上 Transaction,這時候就不必去設定什麼 allowCreate,直接就可以正常運作了!

2008年7月1日 星期二

Hibernate Session 的 get 與 load

Hibernate Session 為了節省一次 Database Roundtrip,所以:
  • load 只是產生一個 Proxy,或是直接取用目前 Session 中已有的 Cache,不會真的連到 Database
  • get 才會真的連到資料庫,取得一個 Fully Initialized POJO
以前只注意到 get 找不到資料的時候會回傳 null,可是 load 不會。不管 Database 有沒有那筆紀錄,load 的傳回值一定不是 null。詳細探究原因,才知道 load 根本不會殺到 Database,也就沒有機會給 null。

org.hibernate.Session 的 Javadoc 說法是:
  • load:Return the persistent instance of the given entity class with the given identifier, assuming that the instance exists. You should not use this method to determine if an instance exists (use get() instead). Use this only to retrieve an instance that you assume exists, where non-existence would be an actual error.
  • get:Return the persistent instance of the given named entity with the given identifier, or null if there is no such persistent instance. (If the instance, or a proxy for the instance, is already associated with the session, return that instance or proxy.)
這邊看不出為什麼 get 不會拿到 Proxy,但是從 org.hibernate.impl.SessionImpl 的程式碼可以看到:
public Object load(Class entityClass, Serializable id, LockMode lockMode) throws HibernateException {
return load( entityClass.getName(), id, lockMode );
}

public Object load(String entityName, Serializable id, LockMode lockMode) throws HibernateException {
LoadEvent event = new LoadEvent(id, entityName, lockMode, this);
fireLoad( event, LoadEventListener.LOAD );
return event.getResult();
}

public Object get(Class entityClass, Serializable id, LockMode lockMode) throws HibernateException {
return get( entityClass.getName(), id, lockMode );
}

public Object get(String entityName, Serializable id, LockMode lockMode) throws HibernateException {
LoadEvent event = new LoadEvent(id, entityName, lockMode, this);
fireLoad(event, LoadEventListener.GET);
return event.getResult();
}

LoadEventListener 有 LOAD 與 GET 兩種情形。

org.hibernate.event.LoadEventListener 的程式碼中可以看出:
public static final LoadType GET = new LoadType("GET")
.setAllowNulls(true)
.setAllowProxyCreation(false)
.setCheckDeleted(true)
.setNakedEntityReturned(false);

public static final LoadType LOAD = new LoadType("LOAD")
.setAllowNulls(false)
.setAllowProxyCreation(true)
.setCheckDeleted(true)
.setNakedEntityReturned(false);

其中,GET 的時候,setAllowProxyCreation 的參數是 false,應該就表示 GET 的時候不允許產生 Proxy 物件。setAllowNulls 的參數是 true,這應該就是為什麼找不到資料的時候可以回傳 null 的原因吧!

2008年6月30日 星期一

兩個 Blogspot 工具

FORMAT MY SOURCE CODE FOR BLOGGING 這個 JavaScript 程式做的還蠻方便的,不然我還不知道要如何張貼程式碼到 Blogspot 呢!後來又逛到文章中引用程式碼的作法,感覺也不錯。

Setup and configuration for New Blogger Tag Cloud / Label Cloud 讓我的 Blogspot 網誌也可以有 Tag Cloud 功能耶!

JBoss AS 搭配 MySQL XA DataSource 設定方式

多方參考網路上的設定,測試之後,發現以下的內容就足夠了:
<?xml version="1.0" encoding="UTF-8"?>
<datasources>
<xa-datasource>
<jndi-name>YourDataSourceName</jndi-name>
<xa-datasource-class>
com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
</xa-datasource-class>
<xa-datasource-property name="URL">
jdbc:mysql://localhost:3306/DBName
</xa-datasource-property>
<user-name>YourUsername</user-name>
<password>YourPassword</password>
<track-connection-by-tx>
true
</track-connection-by-tx>
<new-connection-sql>
set autocommit=1
</new-connection-sql>
<no-tx-separate-pools>
true
</no-tx-separate-pools>
<exception-sorter-class-name>
org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter
</exception-sorter-class-name>
<metadata>
<type-mapping>mySQL</type-mapping>
</metadata>
</xa-datasource>
</datasources>
檔案名稱按照 JBoss AS 的規定,必須以「-xa-ds.xml」結尾,比方說「yourdb-xa-ds.xml」,放在使用組態下的 deploy 目錄即可。

這邊有一件事要注意到,因為 JBoss 本身處理的不夠完善,所以像底下這樣 Pretty-Format 過的寫法會出問題:
<xa-datasource-property name="URL">
jdbc:mysql://localhost:3306/DBName
</xa-datasource-property>
必須改成底下的寫法:
<xa-datasource-property name="URL">jdbc:mysql://localhost:3306/DBName</xa-datasource-property>
這種 Bug 至少在 2003 年 8 月 JBoss 就已經承認,詳見這裡,可是到現在已經 5.0.1 GA 版本了,還是依然存在。

Hibernate 與 MySQL 的 XA DataSource

這幾天一直在測 Hibernate 與 JTA,所以就一直試著設定 MySQL 的 MysqlXADataSource。

奇怪的是,JDBC 搭配 XA DataSource 一直沒問題,可是 Hibernate 搭配 XA DataSource 一直都不成功,不斷顯示 null source 這個訊息。

Google 搜尋了半天,好不容易看到有人也有類似的問題。解法也很爆笑,把 MySQL Connector/J 從 5.1.6 Downgrade 到 5.0.8 版,就搞定了!

2008年6月25日 星期三

科技宅男+氣質正妹+冷眼路人的3P羅生門

以下是昨天晚上發生在某星x客的真實事件,如果雷同,那一定是因為你自己對號入座的緣故。

科技宅男:
小姐,麻煩幫我看一下我的電腦,我下去買杯咖啡。

氣質正妹:
喔!

冷眼路人內心的OS:
奇怪,難不成這樣說一句,其他人就得幫你看好電腦喔?
東西掉了我要賠嗎?
我要尿尿怎麼辦?
我要先走怎麼辦?
萬一你像丟棄嬰一樣不回來了怎麼辦?

五分鐘之後,宅男回來了。

科技宅男:
小姐,謝謝。

氣質正妹:
喔!

科技宅男:
小姐,你有愛趴喔!你那是幾G的?8G還是4G?

氣質正妹:
喔!8G!

科技宅男:
小姐,8G你裝的完喔?

氣質正妹:
還好,謝謝!

正妹戴上耳機,不想理了。

冷眼路人內心的OS:
人家已經說謝謝了,不要再盧了!

科技宅男:
小姐,你可以買4G的,不然也可以買Nano,小小的很方便,價格也差不多。

正妹拿下耳機。

氣質正妹:
喔!我知道。謝謝!

冷眼路人內心的OS:
已經說第二次謝謝了,你聽不懂嗎?

科技宅男:
小姐,那妳的MP3都是下載的,還是妳都有買原版CD?

氣質正妹:
喔!我有買CD。

科技宅男:
是喔!小姐,這年頭會買原版CD的人真的太少了!那妳都聽哪一類的音樂?

氣質正妹:
喔!都聽。謝謝!

正妹戴上耳機,不想理了。

科技宅男:
是喔!小姐,我都聽爵士的!

正妹拿下耳機。

氣質正妹:
喔!謝謝!

正妹戴上耳機,不想理了。

科技宅男:
小姐,那妳知不知道愛趴可以下載專輯封面,這樣看起來很好看。

正妹拿下耳機。

氣質正妹:
喔!我不需要,謝謝!

冷眼路人內心的OS:
已經不知道說第幾次謝謝了,你還是聽不懂嗎?

正妹戴上耳機,不想理了。

科技宅男:
小姐,那妳知不知道愛趴可以播影片?

正妹拿下耳機。

氣質正妹:
嗯。

科技宅男:
我有一陣子花了一堆時間把影片轉成愛趴可以播的格式,Resolution看起來好好喔!

氣質正妹:
嗯。謝謝!

正妹戴上耳機,不想理了。

科技宅男:
小姐,那妳知不知道愛趴可以可以聽廣播?我告訴妳喔,有一種東西叫做Podcasting,妳知不知道?

正妹拿下耳機。

氣質正妹:
嗯。

科技宅男:
小姐,那個Podcasting啊,就是...,對了,妳知道iTunes吧?你的MP3都是用iTunes管理的吧?

氣質正妹:
嗯。謝謝!

正妹戴上耳機,不想理了。

科技宅男:
小姐,那妳有在用電腦吧?

正妹拿下耳機。

氣質正妹:
嗯,我很少用。

科技宅男:
是喔,沒關係。那妳有在用MSN嗎?

氣質正妹:
沒有,很少。

冷眼路人內心的OS:
終於露出你的目的了吧!

科技宅男:
是喔,沒關係。那我給妳我的MSN,如果有問題的話,妳可以MSN我。你有紙筆嗎?借我寫一下...。
這是我的MSN,如果不會操作的話,我可以教妳。
說不定有緣的話,我們還會碰面喔!

冷眼路人內心的OS:
了不起!應該不是有緣,是不幸吧!

這時氣質正妹也露了一手,拿起了手機。

氣質正妹:
喂,XX啊!我這邊差不多好了,你那邊呢?喔!你也好了啊!什麼?你已經到了啊!好,那我下去找你,881!

然後,氣質正妹東西收一收,頭也不回的就走了,留下一臉錯愕的科技宅男。

科技宅男不到五分鐘,也走了!

結論就是:

我還真是溫良恭儉讓啊!上面的手法,我還真是作不出來也說不出口啊!

以上,報告完畢!

2008年5月27日 星期二

Eclipse/WTP Europa 記趣

兩件好玩的事情

1. Ubuntu 執行 Eclipse 的時候會抱怨

"Could not initialize the application's security component. The most likely cause is problems with files in your application's profile directory. Please check that this directory has no read/write restrictions and your hard disk is not full or close to full. It is recommended that you exit the application and fix the problem. If you continue to use this session, you might see incorrect application behaviour when accessing security features."

解決方式也很簡單根據 Bug #188380 in eclipse (Ubuntu) 的說法,只要在 ~/.mozilla 裡面建立一個叫作 eclipse 的目錄即可。

2. 搭配 JBoss AS 的時候這一的 WTP 居然不能直接部署 WAR 檔案必須要封裝成 EAR 檔案才可以

解決方式也很簡單裝上 JBoss Tools在定義 Server Runtime 的時候不要選 WTP 提供的 JBoss、JBoss v4.2用 JBoss, a division of Red Hat 裡面的 JBoss 4.2 Runtime這樣定義出來的 Server 就可以在 WTP 裡面直接將 Dynamic Web Project 拿來執行了