使用IBM Rational Application Developer輕松實現JavaServer Faces Web程序的全球化
在 RAD V7 或者後續版本中使用 JavaServer Widgets Library(JWL)
了解如何使用 IBM Rational Application Developer 來實現 JavaServer Faces Web 程序的全球化。本文描述了開發全球市場所面臨的挑戰,並介紹了怎樣使用 JavaServer Faces Widget Library(JWL)來處理這個問題。
從版本 7 開始,IBM®Rational®Application Developer 包含了 JavaServer Faces Widget Library(JWL),它是一個 Java™Server Faces (JSF)- 以及用於快速開發網絡程序的基於 JavaScript 的庫。
JWL,hxclient 的 JavaScript 庫,實施了對 JWL 構件的客戶端支持。它還包含了一系列所謂的“JSF 轉化器”,可以幫助開發員分析和格式化日期,時間以及特定位置模式的來回號碼,更特別的是,這些工具就是 JavaSimpleDateFormat 和 DecimalFormat 的 JavaScript 實施。這些工具對於設計成支持多種語言的程序來說十分有用,因為它們幫助您處理來自客戶端位置敏感數據輸入和輸出的挑戰。
本篇文章還解釋了與 JavaServer Faces 程序中多線程相關的全球化挑戰問題,並提供了一個解決方案。本文作者假設您有關於 JSF 和 JWL 的基礎知識。
全球化基礎知識
在網絡程序中,輸出語言是由 HTTP 請求報頭的 Accept-Language 區域所決定。用戶可以指定喜好的語言和帶有浏覽器設置的場所。
JSF 框架分析 HTTP 請求報頭。您可以通過使用如列表 1 所示的報頭來獲取該值。
列表 1. 獲取關於語言和場所的請求
Locale locale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
場所值用於決定用於顯示的預言。
使用 JWL 來處理場所敏感輸出與輸入
在快速引入全球化之後,現在我們已經做好准備,討論全球化 JSF 網絡程序中面臨的兩個挑戰:
使用本地格式顯示客戶日期和時間
顯示和結束本地數字格式
hxclient 在頁面中是怎樣初始化的
只要您在使用頁面中的 JWL 標簽,您就必須確保頁面中安裝有庫,並得到了合適的初始化。如果您設置了浏覽器場所請求為“ja”(日語),並查看使用 JWL 調用的 HTML 頁面的源代碼,您將會發現如列表 2 所示的代碼。
列表 2. 使用 JWL 的網絡頁面的結構
<script type="text/JavaScript" language="JavaScript"
src="/sample/.ibmjsfres/hxclient_core_v3_0_8.js"></script>
<script type="text/JavaScript" language="JavaScript"
src="/sample/.ibmjsfres/hxclient_S_v3_0_8_ja.js?viewLocale=ja">
</script>
<script type="text/JavaScript" language="JavaScript">
if (hX_5) hX_5.setResourceServer("/sample/.ibmjsfres");
if(hX_5 && hX_5.setLocale) hX_5.setLocale("ja");
</script>
該代碼可以完成三件事:
包含頁面上的 hxclient 內核腳本庫
包含頁面上的 hxclient 場所特定的腳本庫
創建當前的頁面場所
正如您所看到的那樣,您不需要手動創建場所,因為 JWL 會通過閱讀場所請求來自動決定場所。對於 hxclient 的自動初始化,您已經做好准備將其用於日期,時間和數字格式了。
使用 JWL 的本地格式來顯示客戶日期和時間
對於網絡程序,開發員想要顯示頁面上的最新請求時間。在全球化的程序中,時間必須是當地格式的。
例如,一個美國的用戶可能會想要按以下方式查看日期時間格式 :
Last Refresh: Friday, May 8, 2009 1:35:07 PM GMT+08:00
但是一個日本的用戶也許會看到如下所示的日期時間格式:
前回の最新表示: 2009 年 5 月 8 日金曜日 13 時 41 分 07 秒 GMT+08:00
因為時間是在客戶端計算的,所以 JavaScript 並沒有通過內置 API 來提供一個方案:Date.toLocaleString(). 這是因為:
返回值的場所並不是由浏覽器的場所設置所決定的,而是由客戶操作系統的場所決定。
開發員沒有機會指定日期的格式。
日期時間轉換器是 JWL 客戶腳本庫的一個工具。它是 Java SimpleDateFormat 類的 JavaScript 實施,它可以很好的支持 ICU4J(Unicode Java 庫的國際構件 Library)。它使得客戶端的日期/時間格式變得像處理 Java™一樣容易。在本例中,我們使用 DateTimeConverter 和 ICU4J 來生成客戶端的本地日期/時間。
為了快點開始,讓我們來看客戶端的腳步是什麼樣的:
列表 3. JavaScript 的日期/時間格式
function getLocalizedCurrentTime()
{
var converter = hX.getConverterById("date_converter");
if(null == converter)
{
//construct a new DateTimeConverter and add it to converter set
hX.addConverter("date_converter", new hX.DateTimeConverter(
"format:EEEE, MMMM d, yyyy h:mm:ss a z",
"ICU4J:true"));
}
converter = hX.getConverterById("date_converter");
var date = new Date();
//format client date and return
return converter.valueToString(date);
}
您所要做的只是定位日期/時間。但是這些參數會傳遞給 DateTimeConverter 構建器:
“ICU4J:true” 允許 DateTimeConverter 接受模式的特定 ICU 特征。
“format:EEEE, MMMM d, yyyy h:mm:ss a z” 意味著 DateTimeConverter 用於格式化日期/時間的模式。
到目前為止,我們已經向您展示了,您需要什麼客戶代碼來格式化日期/時間。但這並不足夠。考慮一下模式。您知道對於不同的場所模式會是什麼樣的嗎?
例如,對於美國用戶的模式是這樣的:
EEEE,MMMM d,yyyy h:mm:ss a z
而日本客戶的模式是這樣的:
yyyy'年'M'月'd'日'EEEE H'時'mm'分'ss'秒'z
還好模式並不是硬代碼的,因為開發員並不可能知道不同場所的所有模式。像 “yyyy mm dd”這樣的模式對於某個開發員來說可能是合理的,但是對用戶看來就是很古怪的 。
因此,答案就是從服務器端獲取模式,因為 ICU4J 已經為所有開發員的使用准備好了模式。列表 4 中的代碼展示了,我們怎樣根據本地的請求獲取一個模式:
列表 4. 通過使用 ICU4J 來生成日期/時間模式
public class FormatterUtils
{
public static String getDateTimePattern(int dateFormat)
{
Locale locale =
FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
CalendarData calData = new
CalendarData(ULocale.forLocale(locale), null);
String[] dateTimePatterns = calData.getStringArray("DateTimePatterns");
return dateTimePatterns[dateFormat + 4] + " " + dateTimePatterns[dateFormat];
}
}
參數 dateFormat 顯示了格式模式使用的方法。基本上,在 ICU com.ibm.icu.text.DateFormat 類中有四種預定義的類型:
Full
Long
Medium
Short
對於不同格式的參數,方法 getDateTimePattern 的返回值也有所不同。例如,對於 en-us 場所,有四種類型的返回值:
EEEE,MMMM d,yyyy h:mm:ss a z
MMMM d,yyyy h:mm:ss a z
MMM d,yyyy h:mm:ss a
M/d/yy h:mm a
所以通過從四個類型中選擇一個,您已經准備好了格式模式,讓我們假設您使用的是 Full 格式。下一步是將該格式模式應用到客戶端,以使用 hxclient。在 Java™Server Pages(JSP™)腳本中,這很容易做到。對於客戶端的模式,JavaScript 代碼應該像列表 5 所示。
列表 5. 將格式模式捆綁到客戶代碼
<script>
var datetimeFormatPattern = "<%=FormatterUtils.getDateTimePattern()%>";
function getLocalizedCurrentTime()
{
var converter = hX.getConverterById("date_converter");
if(null == converter)
{
//construct a new DateTimeConverter and add it to converter set
hX.addConverter("date_converter", new hX.DateTimeConverter(
"format:" + datetimeFormatPattern, "ICU4J:true"));
}
converter = hX.getConverterById("date_converter");
var date = new Date();
//format client date and return
return converter.valueToString(date);
}
</script>
現在已經完成了。通過調用 JavaScriptgetLocalizedCurrentTime 方法,您可以得到客戶端的日期和時間文本(例如,圖 1 顯示是英語,圖 2 顯示的是日語)。
圖 1. 顯示 en-us 的日期和時間
圖 2. 顯示 ja-jp 的日期和時間
顯示和接受本地 JWL 的數字格式
顯示帶有本地格式的數字,與顯示日期/時間相類似。像 1000.1 這樣的十進制數字在美國應該顯示成 1,000.1,而在德國會顯示成 1.000,1。
不像日期和時間,這些數字應該從服務器端獲得,在這裡 ICUDecimalFormat 可以完成這一點。但是有些情況下,您可能想要格式化客戶端的數字(例如,接受用戶輸入並顯示值)。在 JWLhxclient 庫中, NumberConverter 實施了客戶端上 DecimalFormat 的邏輯。
同樣,讓我們首先直接跳到完整的客戶代碼:
列表 6. 格式數字的 JavaScript
<script>
var decimalFormatPattern = "<%= FormatterUtils.getDecimalFormatPattern() %>";
var decimalFormatSymbols = "<%= FormatterUtils.getDecimalFormatSymbols() %>";
function formatDecimal(value)
{
var converter = hX.getConverterById("number_converter");
if(null == converter)
{
hX.addConverter("number_converter",
new hX.NumberConverter("pattern:" + decimalFormatPattern,
"locale:" + decimalFormatSymbols, "ICU4J:true"));
}
converter = hX.getConverterById("number_converter");
var output = cvt.valueToString(value);
return output;
}
</script>
有三種參數會傳遞給 NumberConverter 構建器:
“ICU4J:true”:它允許 NumberConverter 接受在模式中 ICU 特定的特征。
“pattern:”+ decimalFormatPattern: 這是在轉化值的時候會用到的數字模式。
“locale:” + decimalFormatSymbols: 這是轉化值時會用到的場所信息。它包含了十進制分隔符,百分比字符等等之類的符號。
至於 DateTimeConverter,模式和場所信息應該從服務器端獲得:
列表 7. 使用 ICU4J 來生成數字模式和符號
public class FormatterUtils
{
private static DecimalFormat getDecimalFormatter()
{
Locale locale =
FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
return (DecimalFormat)NumberFormat.getInstance(locale);
}
public static String getDecimalFormatPattern()
{
return getDecimalFormatter().toPattern();
}
public static String getDecimalFormatSymbols()
{
Locale locale =
FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
//The information is provided as a string of 6 characters with fixed format:
StringBuilder sb = new StringBuilder();
sb.append(symbols.getGroupingSeparator());
sb.append(symbols.getDecimalSeparator());
sb.append(symbols.getPercent());
sb.append(symbols.getPerMill());
sb.append(symbols.getMinusSign());
sb.append(symbols.getCurrencySymbol());
return sb.toString();
}
}
正如您在列表 6 中看到的那樣,getDecimalFormatPattern 和 getDecimalFormatSymbols 用於傳遞頁面中的模式和場所信息。對於服務器端的協助,您可以使用 JavaScriptformatDecimal() 功能,來格式化 JavaScriptNumber 類型變量。列表 8 向您展示了一個這樣的例子。
列表 8. 使用客戶代碼來格式化數字
<script>
//Suppose current locale is "de"
var value = 1000.1; //type of value is Number
var formatted = formatDecimal(value); //the formatted value is "1.000,1" in Germany
</script>
頁面中的數字通常會像圖 3 那樣格式化(該例展示了德語中的匯容量統計):
圖 3. 頁面中的格式化數據:
在處理數字時,不但要注意輸出還要注意輸入。輸入隨著用戶的習慣而不同。一個德國的用戶可能會輸入 1.000,1 或者 1000,1。但是這兩種格式的數據都應該識別為十進制的數據 1000.1。對於開發員來說,這是一個艱難的任務,因為他們需要寫上千行的代碼以識別輸入。
好的消息是 JWLhxclient 可以轉換數字。您可以使用該功能來將用戶輸入轉化為 JavaScript Number 對象。該對象通過自動執行這些步驟,來將顯示的數字和值區別開來:
接受用戶輸入。
通過使用 NumberConverter,來分析 String 對象的輸入到 Number 對象。
使用轉化值以進行計算。
通過再次使用 NumberConverter,來將計算結果格式化回至 String 對象。
使用格式化的值以進行顯示。
列表 9 中的代碼舉了一個例子,展示了怎樣分析用戶輸入(對於 Deutsch 或者 German,場所是“de”):
列表 9. JavaScript 分析的輸入數字
<script>
var decimalFormatPattern = "<%= FormatterUtils.getDecimalFormatPattern() %>";
var decimalFormatSymbols = "<%= FormatterUtils.getDecimalFormatSymbols() %>";
function formatDecimal(input)
{
var converter = hX.getConverterById("number_converter");
if(null == converter)
{
hX.addConverter("number_converter",
new hX.NumberConverter("pattern:" + decimalFormatPattern,
"locale:" + decimalFormatSymbols, "ICU4J:true"));
}
converter = hX.getConverterById("number_converter");
var output = cvt.stringToValue(input);
return output;
}
var parsedValue = formatDecimal("1.000,1"); //the parsed value is 1000.1
parsedValue = formatDecimal("1000,1"); //the parsed value is 1000.1
parsedValue = formatDecimal("oops"); //parsing fails, null is returned
</script>
列表 9 中的代碼在以下方面與列表 6 十分相似:從服務器端獲取模式和場所信息,創建一個 NumberConverter 的范例,然後執行該任務。唯一的區別是調用的方法:stringToValue(). 方法的名字是不言而喻的:它分析一個 String 對象,並試著將其轉化為 Number 對象。如果在轉化期間發生了什麼錯誤,那麼該方法將會返回 null。因此,NumberConverter 也可以用於識別用戶輸入。
到目前為止,我們已經介紹了 JWLhxclient 腳本是怎樣幫助您處理客戶端的數字分析和格式問題。在例子中的代碼中,我們總是需要得到服務器端的場所信息,以生成格式模式。因此,在接下來的章節中,我們將會討論更高級的話題,就是場所信息是怎樣傳遞的,以及在 JSF 網絡程序中是怎樣使用這些信息的。
在多線程 JSF 程序中全球化道路的風險
使用 JSF 進行全球化很容易,但是並不是極簡單的。特別是在多線程的程序中,如果設計缺乏完善的考慮,錯誤的假設將會使得您的全球化支持,變得更像是應用一系列的補丁。接下來我們將要介紹的技術,將會使得您的多線程 JSF 程序變得更加強壯。
決定顯示的語言
全球化通常構建於場所的基礎之上。因此,怎樣實現全球化歸根結底就是怎樣處理場所。在獲取場所信息之後,您已經可以決定基於場所用戶界面中顯示的語言。在大多數情況下,JSF 框架會關注帶有 <loadBundle> 標簽的語言包,它根據場所請求獲取包,而不需要額外的編碼。但是如果您需要使用 Java 代碼中的語言包內容,那麼您就需要使用場所信息來獲取包的路徑,然後自己格式化信息。列表 10 給出了范例代碼。
列表 10. 一個簡單的信息格式化范例
public class MessageFormatter {
private static final String MESSAGE_BUNDLE_NAME = "com.ibm.sample.messages";
private static String formatMessage(String msgKey, Object[] args,Locale locale) {
ResourceBundle messageBundle = ResourceBundle.getBundle(MESSAGE_BUNDLE_NAME,locale);
String message = messageBundle.getString(msgKey);
if (message != null) {
if (args == null) {
return message;
} else {
return MessageFormat.format(message, args);
}
} else {
return msgKey;
}
}
}
得到請求的場所
我們可以看到文本文件中列表 10 所示的代碼,它解釋了怎樣使用 Java 方法來實現全球化。但是在實際操作時當您看到大量 getRequestLocale 存在時是很痛苦的,如列表 1 所示,叫做格式化信息之前。
提示:
查看 IBM Java 技術庫以得到關於 輕松使用線程:不共享有時是最好的變量的更多信息。
接下來,我們將會向您展示怎樣讓工作變得更加完美。
您已經知道,JSFFacesContext 是作為服務線程中的一個 ThreadLocal 變量保存的。因為所有的 Faces backend 豆是在服務線程中運行的,所以您可以在任意時刻獲取 FacesContext 對象。對於場所對象 T 也是真實的,因為它是從 FacesContext 獲取的。這樣您就可以將場所獲取方法放在工具類中,來重寫列表 10 中的代碼,如列表 11 所示。
列表 11. 獲取信息格式化器的最佳方法
public class LocaleUtils(){
public class LocaleUtils{} {
public static Locale getRequestLocale() {
return FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
}
}
public class MessageFormatter {
private static final String MESSAGE_BUNDLE_NAME = "com.ibm.sample.messages";
private static String formatMessage(String msgKey, Object[] args) {
Locale locale = LocaleUtils.getRequestLocale();
ResourceBundle messageBundle = ResourceBundle.getBundle(MESSAGE_BUNDLE_NAME, locale);
String message = messageBundle.getString(msgKey);
if (message != null) {
if (args == null) {
return message;
} else {
return MessageFormat.format(message, args);
}
} else {
return msgKey;
}
}
}
然後 MessageFormatter.formatMessage 就成為從語言包中獲取信息的唯一方法了,它會返回基於請求的信息。現在您可以忽略場所了,因為在 LocaleUtils 的支持下它現在透明地為您工作了。
將請求場所傳遞給用戶線程
如果您想讓場所自動地工作,那麼現在您做的還不夠。
當您在處理多線程程序時,還有一個例外。在多線程程序中,用戶定義的線程並不是由服務線程初始化的,因此它們並沒有將 FacesContext 初始化為一個 ThreadLocal 變量。結果,在該線程中運行的代碼在格式化信息時,就不能訪問場所信息了。
對於以上問題通用的解決方法,是通過在構造變量期間將其傳遞給用戶線程,來讓每一個用戶都有一個場所變量。當變量在線程中准備好時,您可以使用列表 10 中所示的范例代碼,來在用戶線程中運行代碼以實現全球化。但是,這需要編輯用戶線程構造器及其調用的代碼。有一種方法可以讓它更加完美地工作。
我們知道至少有一個用戶線程(或者是它的上級線程)會在服務線程中實現。換句話說,至少有一個用戶線程的構造器在服務線程中得到訪問。因為用戶線程構造器中的代碼仍然在訪問線程的環境下運行,所以在構造用戶線程時,我們還有機會從服務線程繼承場所對象。這樣我們將可以傳遞實現每一個子線程初始化的場所對象。這樣我們就有了 LocaleUtils 的升級版本,這樣通過實施 LocaleSensitive 界面,在用戶線程成為場所敏感的線程之後,在多線程環境中它就可以更好地工作了,就像我們在 BaseThread 中所做的那樣(見於列表 12)。
列表 12. 場所敏感的基底線程
public static Locale DEFAULT_LOCALE = Locale.ENGLISH;
public static Locale getCurrentRequestLocale() {
Locale locale = null;
try {
//try to retrieve locale information from FacesContext
locale = FacesContext.getCurrentInstance().
getExternalContext().getRequestLocale();
}
catch(Throwable e){
//Unable to reach FacesContext
//Therefore call getLocale to retrieve locale variable from current thread
Thread t = Thread.currentThread();
if (t instanceof LocaleSensitive) {
locale = ((LocaleSensitive) t).getLocale();
}
finally{
if(locale == null){
locale = DEFAULT_LOCALE;
}
}
return locale;
}
}
public interface LocaleSensitive {
public Locale getLocale();
}
public class BaseThread extends Thread implements LocaleSensitive {
protected Locale locale= null;
public Locale getLocale() {
return locale;
}
public BaseThread(String name) {
super(name);
//Save a copy of locale in thread instance
this.locale=LocaleUtils.getCurrentRequestLocale();
}
}
public class UserThread extends BaseThread{
public void run(){
…
String message = MessageFormatter.formatMessage(msgKey,msgParameters);
…
}
}
首先,您需要定義一個名為 LocaleSensitive 的界面。任何一個實施該節目的類都應該提供 getLocale() 方法中的場所信息。
然後您使用 BaseThread 來實施 LocaleSensitive 線程。BaseThread 構造器通過訪問 LocaleUtils.getCurrentRequestLocale() 來得到場所對象,它從 FacesContext 中(在服務環境下),或者上級線程中得到場所信息(這就解釋了場所信息是怎樣傳遞的)。然後如果有用戶線程成為 BaseThread 的子類,該線程可以使用 MessageFormatter,而不需升級版本的 LocaleUtils 類,就像我們在服務環境下所做的那樣。
使用列表 12 中的代碼,您就可以在任何線程中傳遞和得到場所信息了,而無需擔心 FacesContext 是否可以訪問。現在多線程 JSF 網絡程序中的風險就不復存在了。
總結
IBM Rational Application Developer 中的 JavaServer Widgets 庫,提供了腳本幫助開發員處理客戶端的全球化問題,這使得全球化開發變得更加容易。在您處理多線程的網絡程序時,要十分謹慎,因為您需要掌握一定的技巧,以確保在程序的任何地方都能夠訪問場所信息。