内容


使用 IBM Rational Application Developer 轻松实现 JavaServer Faces Web 程序的全球化

在 RAD V7 或者后续版本中使用 JavaServer Widgets Library(JWL)

Comments

从版本 7 开始,IBM®Rational®Application Developer 包含了 JavaServer Faces Widget Library(JWL),它是一个 Java™Server Faces (JSF)- 以及用于快速开发网络程序的基于 JavaScript 的库。

JWL,hxclient 的 JavaScript 库,实施了对 JWL 构件的客户端支持。它还包含了一系列所谓的“JSF 转化器”,可以帮助开发员分析和格式化日期,时间以及特定位置模式的来回号码,更特别的是,这些工具就是 JavaSimpleDateFormatDecimalFormat 的 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>

该代码可以完成三件事:

  1. 包含页面上的 hxclient 内核脚本库
  2. 包含页面上的 hxclient 场所特定的脚本库
  3. 创建当前的页面场所

正如您所看到的那样,您不需要手动创建场所,因为 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(). 这是因为:

  1. 返回值的场所并不是由浏览器的场所设置所决定的,而是由客户操作系统的场所决定。
  2. 开发员没有机会指定日期的格式。

日期时间转换器是 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 中看到的那样,getDecimalFormatPatterngetDecimalFormatSymbols 用于传递页面中的模式和场所信息。对于服务器端的协助,您可以使用 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 对象。该对象通过自动执行这些步骤,来将显示的数字和值区别开来:

  1. 接受用户输入。
  2. 通过使用 NumberConverter,来分析 String 对象的输入到 Number 对象。
  3. 使用转化值以进行计算。
  4. 通过再次使用 NumberConverter,来将计算结果格式化回至 String 对象。
  5. 使用格式化的值以进行显示。

列表 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 库,提供了脚本帮助开发员处理客户端的全球化问题,这使得全球化开发变得更加容易。在您处理多线程的网络程序时,要十分谨慎,因为您需要掌握一定的技巧,以确保在程序的任何地方都能够访问场所信息。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Rational
ArticleID=431844
ArticleTitle=使用 IBM Rational Application Developer 轻松实现 JavaServer Faces Web 程序的全球化
publish-date=09292009