[Struts]在jsp里处理比较复杂的内容?

今天遇到一个问题,到现在也没能比较圆满的解决,是不是Struts在标签库上还不够完善呢。比如有一个界面是显示课件列表的,在最后一栏里可以对数据进行操作,如下所示:

Code Name Author OP
10000001 风洞模型课件 刘金东 View Edit Delete
10000002 卡门涡阶课件 季铭义 View Edit Delete
10000003 复变函数课件 秦江 View Edit Delete
10000004 听力课件 郭长凯 View Edit Delete

现在希望当用户按删除时先弹出个确认框,提示“是否确认删除风洞模型课件”,用户可以选择确认或取消。其中“是否确认删除”是在资源文件里定义的(prompt.confirm.delete=是否确认删除{0}),“风洞模型课件”是课件的名称,课件bean名为”ware”。如果写成HTML,就是:

<a href="" onclick="return confirm('是否确认删除风洞模型课件')">Delete</a>

但因为信息都是动态的,所以就有问题了。因为在<html:link>的onclick=”…”里,”<%”必须紧跟在第一个单引号后才能正确解析,即不能写为onclick=”return confirm(‘<%=str%>’)”。所以,现在有两种方法实现所需要的功能:

1、不用<html:link>,直接用HTML的<a>标记:

<bean:define id="toDel" name="ware" property="name" type="String"/>
 <a href="delete.do?code=<bean:write name="ware" property="code"/>"
     onclick="return confirm('<bean:message key="prompt.confirm.delete" bundle="root" arg0="<%=toDel%>"/>');">
     <bean:message key="course.list.op.delete"/>
 </a>
 

2、使用<html:link>,事先定义一个只含一个参数的script函数,代码如下:

<script language="JavaScript">
 <!--
 function confirmDelete(str){
     return confirm('<bean:message key="prompt.confirm.delete" bundle="root" arg0="'+str+'"/>');
   }
 -->
 </script>
 

然后在删除链接的地方这样写:

<bean:define id="toDel" name="ware" property="name" type="String"/>
 <html:link action="/delete" paramId="code" paramName="ware" paramProperty="code" onclick="<%="return confirmDelete('"+toDel+"');"%>">
     <bean:message key="course.list.op.delete"/>
 </html:link>

这两种方法都能达到目的,我暂时使用了第2种用法,毕竟在struts程序的jsp里直接使用<a>标记有点别扭。我看了一下struts文档,能把资源中的{0}转换为实际内容的标签好象只有<bean:message>这一个,其实如果有办法让<bean:message>得到的内容放进某个bean里就很好办了,可惜……。

另外,没研究过EL标签库,不知道会不会有帮助。

反向链接referrer的原理

一直都不明白反向链接是怎么实现的,今天好像有点懂了。在这个网站看到,原来一段javascript代码就够了,一定是利用了浏览器的什么功能。

<script language="Javascript" src="http://www.downes.ca/referrers.js"></script>

倒,看了一下这个js的内容,原来document还有个referrer属性啊,现在完全没有神秘感了。

function write_ref() {
   document.write("<script language='Javascript' src='http://216.55.133.99/cgi-bin/data/referrers.cgi?in=" + document.referrer + "&out=" + document.location + "'>");
   document.write("</");
   document.write("script>");
}
write_ref();

不过博客园这里好像是不让插入script的,插入的script会被过滤。而且也不能插入form,因为会和搜索的那个form嵌套。

由PPP项目总结的几点项目经验

这些基本都是从老大身上学来的,在PPP项目中起到了积极作用,我认为至少是比较适合六人左右的小项目的。也有的不算是经验,或者说是公认的最佳实践,呵呵。

1、尽可能获得详细的需求,最好把界面先画出来让客户确认,形成文档。毕竟,修改静态的界面比修改程序快多了。

2、有专人负责与客户保持全程接触,此人不设计也不编程。在可能情况下让客户的人员参与项目,项目结束后的维护工作就可以省很多心了。

3、建立两套版本控制,一套负责代码,一套负责文档。交付前,要完全模拟客户环境进行测试,从安装开始。

4、建立bug跟踪系统,专人测试,这一点非常重要,而且测试要尽早进行。PPP项目大多数时间是五人编码,一个测试,测试人员每天都做到晚上10点感觉还是不够用,两人比较好。

5、对于客户提出的需求变化,如果认为不应该修改,要提出原因。但不能对所有的这种要求都否决,可以修改那些不影响模型的。

[Eclipse]国际化你的应用程序(下)

在本文的上篇里,介绍了使用Eclipse的国际化工具对程序中的字符串进行外向化处理(Extenalize),可以看出步骤是十分简单的。实在是很喜欢Eclipse这样的工具,它可以为你做很多事情,干净漂亮,但绝不会在未经你同意的情况下做任何动作,所谓“利器”也!

现在说说在资源中含有参数的情况怎样处理。比如在对话框中要显示信息:“帐户目前还有 900 元,截止日期为 2004-9-1,谢谢!”,因为中间的数字和日期是动态的,所以不能直接放在资源文件中。但是请放心,大可不必为这条信息指定三个资源(被数字和日期分开的三个字符串),可以在资源文件(.properties)中指定资源为这个样子:

my.resource.key=帐户目前还有 {0} 元,截止日期为 {1},谢谢!

其中{0}和{1}表示将替换为动态的值,然后在程序里写:

Float amount = ...;
Date dt = ...;
String msg=MessageFormat.format(Messages.getString("my.resource.key"), new Object[]{amount,dt});

这样,msg变量里就是动态生成的提示信息了。你很可能希望对日期进行格式化处理,要实现这个功能也很简单,只要稍微修改一下资源,如下:

帐户目前还有 {0} 元,截止日期为 {1,date,yyyy-MM-dd},谢谢!

确实简单吧,不知道你用没用过这个写法,我是在PPP项目中才第一次使用的,所以赶紧介绍一下了,呵呵。

接下来的问题是编码,资源文件写为英文是没有问题的,可以正常显示,但汉字是不能直接写在资源文件里的,要转换为unicode才可以。jdk本身提供了native2ascii工具,可以实现这个功能,但用命令行总是不太方便,虽然也有人很喜欢使用命令行的感觉……如果你愿意Eclipse为你服务,大可以使用我下面介绍的两个插件,利用它们,你根本不需要显式转换编码这一步了。

第一个是Properties Editor,好象是日本人写的,安装后它会与扩展名为.properties的文件相关联,使用它打开资源文件,可以在本地语言与unicode视图之间切换,一般情况下编辑本地语言就可以了,保存时会自动转换为unicode。当需要查找某个资源(值)并修改时这个编辑器非常方便,例如想把资源里所有“你好”改为“您好”,如果面对的是一堆unicode码还真是头疼。安装这个插件需要2.1.1版本以上的Eclipse。

另一个我们在项目中经常使用的插件叫……我还真不知道它全名叫什么,在我这里的打包文件名是“29 Localization Editor”,它是一个非常方便的国际化翻译工具。用它打开扩展名为.properties的文件后,可以新建key,或者新建语言,你要做的只是在表格中把尚未翻译成新语言的资源值填下而已,可以选择只显示未翻译的条目或是全部条目。不过很对不起,我还没找到网上的下载地址,如果需要请和我联系吧。

图1 Localization Editor使用界面

好了,关于Eclipse的国际化先介绍到这里了。如果你的应用程序是Eclipse插件,还可以更进一步:把资源文件打成语言包。关于这种方式(Fragment)的介绍,以后有时间再写吧。

[Struts]使用tiles管理界面遇到困难

上个周末都在研究怎么用tiles管理示范中心项目的界面,没想到遇到了不少麻烦,到现在也没解决。首先,示范中心项目有很多个模块,我们是用struts的模块功能分开的。本来想的是在缺省模块里定义几个公用的界面定义(definition),然后再各模块里都继承这个定义,并修改必要的tile就可以了。没想到不管怎么设置,模块里的定义都继承不到缺省的定义。缺省模块里:

<plug-in className="org.apache.struts.tiles.TilesPlugin" >
  <set-property property="definitions-config" value="/WEB-INF/tiles-defs.xml" />
</plug-in>

教师模块里:

<plug-in className="org.apache.struts.tiles.TilesPlugin" >
  <set-property property="definitions-config" value="/WEB-INF/tiles-defs.xml,/WEB-INF/tiles-defs-teacher.xml" />
</plug-in>

tiles-defs.xml里:

<definition name="classicLayout" path="/layout/classic.jsp">
    <put name="header" value="/header.jsp" />
    <put name="menu" value="/teacher/list.do"/>
    <put name="main" value=""/>
    <put name="footer" value="/footer.jsp" /> 
</definition>

tiles-defs-teacher.xml里:

<definition name="listLayout" extends="classicLayout">
    <put name="main" value="/teacher/list.jsp"/>
</definition>

然后在教师模块里forward到listLayout,提示path没有以”/”开头,就是没有找到listLayout这个定义了。我试了很多写法,包括设置moduleAware的属性,都没有成功。

后来想就在每个模块里都写classicLayout的定义吧,都指向同一个.jsp定义文件就可以了。又遇到新问题,我想在teacher模块里显示menu模块里的内容,会提示找不到所需资源,因为我是在teacher模块里,menu模块的资源是无法访的,除非在menu模块的配置文件里指定key,再在.jsp文件里强制指定bundle的名称,我觉得这个方法太不雅了,同时要做不少修改。

<definition name="classicLayout" path="/layout/classic.jsp">
    <put name="header" value="/header.jsp" />
    <put name="menu" value="/menu/list.jsp"/>
    <put name="main" value="/teacher/list.jsp"/>
    <put name="footer" value="/footer.jsp" /> 
</definition>

还有,<put>里的value只能是.jsp吗,用.do行不行,我试的结果是不行,虽然没报任何错误,但页面生成到那之前就截止了。郁闷!

[测试]使用Mantis跟踪bug

在PPP项目中我们组使用PVCS Tracker来跟踪bug,感觉项目组的确是需要这样一套系统的,PVCS虽然功能比较强,但首先不是免费的,另外也比较大,对于示范中心这样的小项目来说有些不够灵活。因此我安装了Mantis,一个十分小巧的bug跟踪工具。

Mantis是php写的开源软件(Bugzilla也是开源的,written in perl,但在windows下安装麻烦,所以暂时不考虑)。安装Mantis的步骤很简单,以下总结在Win2000/XP下的安装过程:

1、如果机器上有IIS,确保已经启动;如果希望使用Apache,从httpd.apache.org下载apache的windows安装程序,我用的是1.3版本,直接运行这个程序就安装完成了。

2、从www.php.net下载php的windows安装程序,我用的是4.3.8版本,也是直接运行下载来的程序。我用IIS时安装程序会自动对IIS进行设置,但在另一台没有IIS而使用Apache的时候,即使在安装过程中选择了正确的服务器类型,也会提示自动设置出错。不过手动设置也很简单,只要在apache安装目录下的conf目录里的httpd.conf里增加这样一段:

ScriptAlias /php/ "c:/php/"
AddType application/x-httpd-php .php
Action application/x-httpd-php "/php/php.exe"

这样就可以了。不过按照PHP的安装说明,这种方式是很危险的,我对PHP不熟,说不出到底危险在哪,可能是容易给Web服务器带来一些安全漏洞吧。

3、从www.mantisbt.org下载mantis的0.8.3版本(原来写成1.8.3是笔误,谢谢wfifi指出。mantis目前已有1.0.0rc版本),它很小只有几百K。如果是IIS,把mantis解压缩到Inetpub下(我一开始解到c:\下总是不行,可能是权限问题),然后增加一个虚拟路径指向mantis目录;如果是Apache,把mantis解压缩到apache的安装目录下,并在httpd.conf里把主目录改为指向这个目录,或者增加一个Alias,但权限要设够才能正常运行。为了方便,可以在Web服务器里增加index.php为缺省文件名。

4、从www.mysql.net下载mysql,我用的是4.0版本。在mysql里为mantis建立一个帐户,然后建立一个名为bugtracker的数据库,这是mantis配置文件里的缺省名字,在mantis安装路径下的sql目录里有一个db_generate.sql文件,这里面是建表的语句,执行它。

5、把mantis安装路径下的config_inc.php.sample改名为config_inc.php,打开并修改里面的内容。主要是和数据库连接的信息,例如数据库名、用户名等等。我在最后加了这样两句:

$g_default_language = 'chinese_simplified';
$g_enable_email_notification = OFF;

这样缺省界面就是简体中文的,并且不发送邮件通知。要发送邮件还要对PHP进行另外的配置,我还没有试过,好象挺麻烦的,所以干脆禁掉。另外mantis提供的简体中文语言包里好象很多“删除”都写成了“.h除”,我对lang目录中的strings_chinese_simplified.txt文件做了一个替换(.h除->删除)就好了。

6、最后,重启一下Web服务器,就可以访问了,地址是http://localhost/mantis这样的。按照mantis的建议,应该新建至少一个administrator级别的用户,然后把admin目录删除,并删除administrator这个帐号。

我们的项目不复杂,而且成员少,所以mantis的安装能用就行,对安全和报表、邮件的配置都没有关心,以后需要用到的时候再研究吧。

Update(2012/11/12): 今天再次看了一下Mantis网站,这个bug跟踪系统又有很多改进,而且增加了手机客户端,下次有机会还要继续使用。

[Struts]处理表单中值为空的日期类型字段

在示范中心项目中,我们把ActionForm中日期类型的字段指定为String类型,而在对应的JavaBean中指定为java.sql.Date类型。当用户提交表单的时候,在Action里使用BeanUtils.copyProperties()方法从ActionForm构造JavaBean对象(详见利用BeanUtils在对象间复制属性)。这个方法在大部分时候都很好,但有一个问题,就是当用户没有填写日期类型字段时(而该字段并非必填),validator不会提出警告,而在copyProperties()时会报类型转换异常,原因是这时ActionForm中的该字段的值是空字符串(””),负责字符串向Date转换的SqlDateConverter类调用Date.valueOf(“”)方法,显然””是无法转换为日期的,所以会抛出异常。

通过查看代码和资料,我发现这个问题的解决方法其实非常简单。只要把带缺省值参数的SqlDateConverter重新注册一下,覆盖原有的注册信息就可以了,这个注册语句一般是写在系统初试化的地方,对于Struts应用程序,当然做在PlugIn里最方便。代码如下:

package etc;

import javax.servlet.ServletException;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.SqlDateConverter;
import org.apache.struts.action.ActionServlet;
import org.apache.struts.action.PlugIn;
import org.apache.struts.config.ModuleConfig;

public class ConverterPlugIn implements PlugIn{

    public void init(ActionServlet servlet, ModuleConfig config) throws ServletException {
        ConvertUtils.register(new SqlDateConverter(null),java.sql.Date.class);
    }

    public void destroy() {
        ConvertUtils.deregister();
    }
}

注意SqlDateConverter的构造方法是带有参数null的,这表示遇到不能解析的字符串就返回空值。而deregister()方法的作用是恢复ConvertUtils的缺省注册表。为了使这个PlugIn起作用,要在struts-config.xml里增加一句话:

<plug-in className="etc.ConverterPlugIn" />

日期字段往往会给我们的开发带来麻烦,其实在Struts应用程序里,只要把这些转换类搞熟了,总可以找到很方便的办法。常见的问题还有如何指定日期输入格式,怎样处理java.util.Date的转换,等等,在这个链接里有解决这些问题的方法,道理都是一样的。

[Hibernate]xDoclet生成hbm的一个bug

做示范中心项目时遇到的,类Teacher实现接口BusinessObject,在接口里用@hibernate.class,在类里用@hibernate.joined-subclass-key column=”oid”和@hibernate.joined-subclass,执行ant任务时只生成了BusinessObject.hbm.xml,而且在里面没有关于Teacher的定义。为此折腾了好一阵,后在在网上找到一个贴子说的是同一个问题,还提供了一个patch,不过还没试好不好使,内容如下。(快该回家了,晚上继续写)

diff -u -1 -b -p -r1.20 HibernateTagsHandler.java
— HibernateTagsHandler.java   14 Jun 2003 13:58:10 -0000      1.20
+++ HibernateTagsHandler.java   3 Nov 2003 00:58:27 -0000
@@ -285,3 +285,7 @@ public class HibernateTagsHandler
                 }
–                else if (clazz.getSuperclass() != null && clazz.getSuperclass().getQualifiedName().equals(typeName)) {
+                else if ((clazz.getSuperclass() != null &&
+                    clazz.getSuperclass().getQualifiedName().equals(typeName))
+                    ||
+                    (getCurrentClass().isInterface() &&
+                    clazz.isImplementingInterface(typeName))) {
                     log.debug(“is a subclass”);

现在决定不用这个patch的方法了,改源码得重新build,而且以后就不能用通用包了。暂时拿抽象类代替接口吧,差不多。

[Eclipse]国际化你的应用程序(上)

记得几年前汉化软件一般是用二进制编辑工具在编译后的文件中查找和替换英文单词,这个过程需要使用很多技巧,非常的麻烦。而现在,在开发时对应用程序进行国际化处理已经越来越成为一个必不可少的步骤了。例如这次我参与的项目是给台湾客户做的,他们要求英文和繁体中文两个版本,幸好我们使用的开发工具是Eclipse,利用它的国际化功能可以很方便的将写在代码里的字符串提出到独立的资源文件中,这里用一个简单的例子说明一下这个过程。

在Eclipse里建立一个名为nls-test的工程(也可以国际化已有工程),新建一个类NLSTest,内容如下:

public class NLSTest {
 public NLSTest() {
  String str1="Hello world!";
  System.out.println(str1);
 }
 public static void main(String[] args) {
  new NLSTest();
 }
}

现在,这个类的输出是在代码里写死的,如果要改变就必须修改代码然后重新编译。下面我们利用Eclipse解决这个问题。在导航器(Package Explorer)里右键单击这个文件,选择Source -> Externalize Strings,就会打开一个对话框,在这里列出了该类中尚未国际化的字符串,见下图。

图1 国际化向导第一步

单击每个字符串前面的小方块可以选择对该串1、进行国际化处理2、永不进行国际化处理3、这次不处理。我们选择要对这个字符串进行国际化处理,并把它的Key修改为比较好理解的名称hello,注意在对话框最上方可以指定一个通用的前缀,这个值会加在每个Key前面作为最终写在资源文件(扩展名是.properties)里的Key名称。好,按下一步继续。

在这里要指定properties文件的名称,如果需要的话要指定一个用于从properties文件中取资源的类,一般是XXXMessages的格式,再按下一步,会显示一个所作更改的确认列表,确认后按Finish按钮完成向导。

可以看到Eclipse为我们生成了NLSTestMessages.java和NLSTest.properties,打开后者会看到里面只有这么一句:

NLSTest.hello=Hello world!

而前者NLSTestMessages的作用是根据参数Key从后者取对应的字符串值,看一看现在的NLSTest.java就知道了,它的内容现在是这样的(只列出构造方法,main方法同上):

public NLSTest() {
  String str1=NLSTestMessages.getString("NLSTest.hello"); //$NON-NLS-1$
  System.out.println(str1);
}

后面的注释是Eclipse自己用的,标有这个注释的字符串在国际化将被忽略。注释中的数字1表示要忽略的是该行中第一个字符串,如果一行语句里有多个字符串被忽略,将会有多个这样的注释,但数字会各不相同,像这样:

//$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

好了,现在这个类的国际化处理就算完成了。要想让这个类可以根据用户所在地区输出不同语言的结果,可以在NLSTest.properties同一目录下创建名为NLSTest_XX.properties的文件,其中XX表示国家名称,例如中国是zh_CN或zh_TW,法国是FR等等。新创建的文件里也要有和原来文件相同的名值对,但值是不同语言的,NLSTestMessages类会根据用户机器的地区设置值自动从不同的资源文件里取值,这样就达到了国际化的目的。要在自己的机器上测试运行结果,可以在Eclipse的运行设置里面加上这样的参数:-nl zh_TW,这样就不用费力气设置区域了。(2012/5/4更新:此方法可能有误,在Eclipse3.6里是启动程序时在VM arguments里加上-Duser.language=XXX即可以所需要的语言环境启动程序)

国际化的原理很简单,Eclipse提供的这个功能使国际化变得更容易了。不过关于国际化还有一些细节问题,包括对含参数资源的处理,字符编码处理等等,下篇将对它们进行讨论。

[Struts]让Dreamweaver显示Struts标签的插件

Dreamweaver(简称DW)的设计视图里不能显示struts标签,只能手动改代码。为此我找了好久,终于还是在DW网站上找到了,只有8K大,虽然没有漂亮的图标,但显示的信息还是很够用的。现在总算可以用DW编辑含有struts标签的jsp文件了!

图1 在DW里显示struts标签

这个文件我已经放在FTP上了,请点这里下载。如果连不上,请用这个地址。下载以后直接双击就安装,然后重开DW就行了。不需要了可以在Extension Manager里卸载。

Update: 感谢dudu帮我开通了文件上传功能,本地下载