乐者为王

Do one thing, and do it well.

世界上最好的推销员的故事

一个乡下来的小伙子去应聘城里“世界最大”的“应有尽有”百货公司的销售员。老板问他:“你以前做过销售员吗?”他回答说:“我以前是村里挨家挨户推销的小贩子。”老板喜欢他的机灵:“你明天可以来上班了。等下班的时候,我会来看一下。”

一天的光阴对这个乡下来的穷小子来说太长了,而且还有些难熬。但是年轻人还是熬到了5点,差不多该下班了。老板真的来了,问他说:“你今天做了几单买卖”

“一单。”年轻人回答说。

“只有一单?”老板很吃惊地说:“我们这儿的售货员一天基本上可以完成20到30单生意呢。你卖了多少钱?”

“300,000美元,”年轻人回答道。

“你怎么卖到那么多钱的?”目瞪口呆,半晌才回过神来的老板问道。

“是这样的,”乡下来的年轻人说,“一个男士进来买东西,我先卖给他一个小号的鱼钩,然后中号的鱼钩,最后大号的鱼钩。接着,我卖给他小号的鱼线,中号的鱼线,最后是大号的鱼线。我问他上哪儿钓鱼,他说海边。我建议他买条船,所以我带他到卖船的专柜,卖给他长20英尺有两个发动机的纵帆船。然后他说他的大众牌汽车可能拖不动这么大的船。我于是带他去汽车销售区,卖给他一辆丰田新款豪华型‘巡洋舰’。”

老板后退两步,几乎难以置信地问道:“一个顾客仅仅来买个鱼钩,你就能卖给他这么多东西?”

“不是的,”乡下来的年轻售货员回答道,“他是来给他妻子买卫生棉的。我就告诉他‘你的周末算是毁了,干吗不去钓鱼呢?’”

PowerScript标识符

在使用减号(-)、负号(-)、减1(--)运算符时,必须在这些符号的前面加上空格,原因在于PowerScript会把这些符号当成标识符的一部分。

PowerScript标识符规则:

  1. 必须以字母或下划线(_)开头;
  2. 其余字符可以是字母、数字、下划线(_)、美元符号($)、短横线(-)、号码符号(#),百分号(%);
  3. 最长40个字符(实际最大长度可达99个),中间不能有空格;
  4. 不区分大小写。

在PB.INI的[pb]节中将DashesInIdentifiers=1修改为DashesInIdentifiers=0可以禁止在标志符中使用短横线(-)。

猎狗与猎人的故事

目标

一条猎狗将兔子赶出了窝,一直追赶他,追了很久仍没有捉到。牧羊看到此种情景,讥笑猎狗说,“你们两个当中小的反而跑得快得多。”猎狗回答说:“你不知道我们两个的跑是完全不同的!我仅仅为了一顿饭而跑,他却是为了性命而跑呀!”

动力

这话被猎人听到了,猎人想:猎狗说的对啊,那我要想得到更多的猎物,得想个好法子。于是,猎人又买来几条猎狗,凡是能够在打猎中捉到兔子的,就可以得到几根骨头,捉不到的就没有饭吃。这一招果然有用,猎狗们纷纷去努力追兔子,因为谁都不愿意看着别人有骨头吃,自己没吃的。就这样过了一段时间,问题又出现了。大兔子非常难捉到,小兔子好捉。但捉到大兔子得到的奖赏和捉到小兔子得到的骨头差不多,猎狗们善于观察,发现了这个窍门,专门去捉小兔子。慢慢地,大家都发现了这个窍门。猎人对猎狗说:最近你们捉的兔子越来越小了,为什么?猎狗们说:反正结果没有什么大的区别,为什么费那么大的劲去捉那些大的呢?

保障

猎人经过思考后,决定不将分得骨头的数量与是否捉到兔子挂钩,而是每过一段时间就统计一次猎狗捉到兔子的总重量,按照重量来评价猎狗,从而决定它们一段时间内的待遇。于是,猎狗们捉到兔子的数量和重量都增加了。猎人很开心。但是,过了一段时间,猎人发现猎狗们捉兔子的数量又少了,而且越有经验的猎狗捉兔子的数量下降得越利害。于是,猎人又去问猎狗是怎么回事。猎狗说,“我们把最好的时间都奉献给了您,主人,随着时间的推移我们会慢慢变老,当我们捉不到兔子的时候,您还会给我们骨头吃吗?”

回报

猎人做出了论功行赏的决定,分析与汇总了所有猎狗捉到兔子的数量与重量,规定如果捉到的兔子超过一定的数量后,即使捉不到兔子,每顿饭也可以得到一定数量的骨头。猎狗们都很高兴,大家都努力去争取达到猎人规定的数量。一段时间过后,终于有一些猎狗达到了猎人规定的数量。这时,其中一只猎狗说:“我们这么努力,只得到几根骨头,而我们捉的猎物远远超过了这几根骨头。我们为什么不能给自己捉兔子呢?”于是,有些猎狗离开了猎人,自己捉兔子去了。

归宿

猎人意识到猎狗正在流失,并且那些流失的猎狗像野狗一般与自己的猎狗抢兔子。情况变得越来越糟,猎人不得已引诱了一条野狗,问他到底野狗比猎狗强在那里。野狗说:“猎狗吃的是骨头,吐出来的是肉啊!”接着又道:“也不是所有的野狗都顿顿有肉吃,大部分最后骨头都没的舔!不然也不至于被你诱惑。”于是,猎人又进行了改革,使得每条猎狗除基本骨头外,可获得其所猎兔肉总量的n%,而且随着服务时间加长,贡献加大,该比例还可递增,并有权分享猎人所拥有兔肉总量的m%。就这样,猎狗们与猎人一起努力,将野狗们逼得叫苦连天,纷纷要求重归猎狗队伍。

故事还在继续……

只有永远的利益,没有永远的朋友

日子一天一天地过去,冬天到了,兔子越来越少,猎人们的收成也一天不如一天。那些服务时间长的老猎狗们已老得捉不到兔子,但却仍然无忧无虑地享受着他们自以为应得的大份食物。终于有一天,猎人再也不能忍受,把他们扫地出门了,因为猎人更需要身强力壮的猎狗……

Birth of MicroBone Co.

被扫地出门的老猎狗们得到一笔不菲的赔偿金,于是,他们成立了MicroBone公司。他们采用连锁加盟的方式招募野狗,向野狗们传授猎兔的技巧,他们从猎得的兔子中抽取一部分作为管理费。当赔偿金几乎全部用于广告后,他们终于有了足够多的野狗加盟,公司开始盈利了。一年后,他们收购了猎人的家当……

Development of MicroBone Co.

MicroBone公司许诺,加盟的野狗可以得到公司n%的股份。这实在是太有诱惑力了。那些自认为怀才不遇的野狗们都以为找到了知音:终于可以做公司的主人了,不用再忍受猎人呼来唤去的不快,不用再为捉到足够多的兔子而累死累活,也不用眼巴巴地乞求猎人多给两根骨头。这一切对野狗们来说,比多得到两根骨头更加受用。于是,野狗们拖家带口地加入了MicroBone。一些在猎人门下服务的年轻猎狗也蠢蠢欲动,甚至很多自以为聪明、实际上愚蠢的猎人也想加入。好多同类型的公司雨后春笋般地成立了,BoneEase、Bone.com、ChinaBone……一时间,森林里热闹非凡。

干活的总是拿得少的,拿得多的都是不干活的

猎人凭借出售公司的钱走上了老猎狗走过的路,历经千辛万苦重新发达起来,最后又来与MicroBone公司谈判收购事宜,老猎狗出人意料地把MicroBone公司卖给了猎人。老猎狗们从此不再经营公司,转而开始靠写作赚钱,写完自传《老猎狗的一生》,又写了《如何成为出色的猎狗》、《成功猎狗500条法则》、《猎狗成功秘诀》、《如何成为一只进入管理层的猎狗》、《穷猎狗,富猎狗》等一系列成功自助读物,并且将老猎狗的故事搬上了屏幕,取名《猎狗花园》——从此,老猎狗成为家喻户晓的名人,坐收版权费,既没有风险,利润又可观,轻轻松松地过上了富人闲人的日子。

无我编程的十诫

英文原文:https://blog.codinghorror.com/the-ten-commandments-of-egoless-programming/

无我编程的十诫最早出现在杰拉尔德·温伯格(Gerald M. Weinberg)的《The Psychology of Computer Programming》中:

  • 理解并接受自己会犯错误。关键是在错误进入到生产环境之前尽早地找到它们。幸运的是,除了我们几个在JPL开发的火箭制导软件外,我们行业中的错误很少是灾难性的,所以我们可以而且应该,学习、大笑、然后继续前行。
  • 你不是你的代码。记住,代码审查的整个出发点是找到问题,而且终归会找到问题。所以当问题被发现时不要太在意。
  • 不管你知道多少“karate”,总有人比你知道得更多。如果你问的话,这样的人可以教你一些新的动作。寻求并接受他人的意见,尤其是当你认为不需要的时候。
  • 不要没有商讨就重写代码。 “修复代码”和“重写代码”之间有着明显的区别。弄清两者的区别,并在代码审查的框架内追求风格变化,而不是孤独的执行者。
  • 尊重比你懂得少的人,并抱以耐心。与开发者交涉的非技术人员几乎普遍认为我们是妄自尊大的讨厌鬼。不要用愤怒和不耐烦来加深这种刻板印象。
  • 世界上唯一不变的就是变化。敞开胸怀,面带微笑地去接受变化。将需求、平台或工具的每个变化视为新的挑战,而不是一些严重地需要抗争的不便。
  • 真正的权威源自知识,而不是职位。知识产生权威,权威就会产生尊重——所以如果你想在无我的环境中得到尊重,请充实知识。
  • 为信念而战,但优雅地接受失败。明白有时你的想法将被推翻。即使你的确是正确的,也不要报复或者嚷嚷说“我早就说过”,不要把被否定的想法当作是牺牲品或者口号。
  • 不要成为“宅男”。不要成为在黑暗的办公室里编码的家伙,就算偶尔露个面,也只是为了买杯可乐。“宅男”会与其他人失去联系,淡出他们的视野,失去控制,在开放协作的环境中将没有任何位置。
  • 批评代码而不是人——对程序员好点,而不是代码。尽可能地使你的所有评论都是积极的,并且旨在改进代码。将评论与局部标准、程式规格、性能提升等相关联。

软件的人性化原则是永恒的。《The Psychology of Computer Programming》早在1971年我1岁时就写成。虽然已经过去几十年,但这些原则并没有过时,仍然值得所有程序员拜读。

Ajax:Web应用的一种新方法

英文原文:http://adaptivepath.org/ideas/ajax-new-approach-web-applications/

如果有任何关于当前的交互设计可以被称为“迷人”的话,那就是创建Web应用。毕竟,你最后一次听到有人吹嘘不在Web上的产品的交互设计是什么时候?(好吧,除了iPod。)一切都很酷,富有创意的新设计都是在线的。

尽管如此,Web交互设计师仍不能不对创建桌面软件的同事们感到一丝羡慕。桌面应用有着在Web上似乎遥不可及的丰富和响应能力。让Web迅速扩散的同样简单也造成了在我们可以提供的体验和用户能够从桌面应用得到的体验之间差距。

这一差距正在缩小。看一看Google Suggest。观察随着你输入建议项更新的方式,几乎立即。现在看看Google Maps。放大。使用鼠标抓取地图并稍稍滚动。再次滚动,一切几乎立即发生,无需等待页面重新加载。

Google Suggest和Google Maps是Web应用的一种新方法的两个例子,该方法在Adaptive Path被我们称为Ajax。这个名字是异步JavaScript + XML的简写,它代表了Web上可能的根本性转变。

定义Ajax

Ajax不是一种技术。它事实上是几种技术,每种技术都在它自身正确的地方蓬勃发展,以强大的新方式聚合在一起。Ajax包括:

经典的Web应用模型是这么工作的:在界面上的大多数用户操作触发一个HTTP请求回Web服务器。服务器做一些处理——检索数据,精密计算,与各种遗留系统对话——然后返回一个HTML页面到客户端。它是改编自Web作为超文本媒介的原定用途的一个模型,但是如同用户体验的要素的爱好者所知,该模型让Web很好地适合超文本不一定能让它很好地适合软件应用。

图1:Web应用的传统模型(左)与Ajax模型(右)的比较。

这种方法很有技术上的感觉,但它无助于很好的用户体验。当服务器在做它的事情的时候,用户在做什么?没错,等待。在任务中的每一步,用户都在等待。

显然,如果我们从头开始为应用设计Web,我们不会让用户呆呆地等待。一旦界面被加载后,为什么每次应用需要从服务器获得什么时用户交互就将陷入停顿?事实上,为什么用户应该看到所有进入服务器的应用?

Ajax有何不同

Ajax应用消除了在Web上交互的开始-停止-开始-停止的本质,通过引入中介——Ajax引擎——在用户和服务器之间。它看起来像是给应用添加一个层而让应用更少地作出响应,但事实恰好相反。

在会话开始而不是加载页面的时候,浏览器加载Ajax引擎——用JavaScript编写并且通常藏在一个隐藏的Frame中。在用户方面引擎同时负责渲染用户看到的界面和与服务器通讯。Ajax引擎允许用户和应用的交互异步发生——独立于与服务器的通讯。因此用户永远不会开始于一个空白浏览器窗口和一个沙漏图标,呆呆地等待服务器做某些事情。

图2:传统Web应用的同步交互模式(上)与Ajax应用的异步模式(下)的比较

通常会生成一个HTTP请求的每个用户操作采取JavaScript调用Ajax引擎的形式替代。任何对用户操作的响应不需要回到服务器——例如简单的数据验证,编辑内存中的数据,甚至一些导航——引擎自己处理。如果为了响应引擎需要从服务器获得什么——如果这是提交数据以进行处理,加载额外的界面代码,或者检索新的数据——引擎异步发出这些请求,通常使用XML,没有拖延用户和应用的交互。

谁在使用Ajax

Google在开发Ajax方法上做了巨大的投资。在过去一年里Google推出的所有主要产品——OrkutGmailGoogle Groups的最新beta版本、Google Suggest,以及Google Maps——都是Ajax应用。(想了解更多这些Ajax实现的技术细节,可以查看GmailGoogle Suggest,和Google Maps的这些优秀分析)别人也在模仿:Flickr中人们喜欢的许多特性依靠Ajax,而Amazon的A9.com搜索引擎已经运用了类似的技术。

这些项目表明Ajax并不只是技术层面的论调,而且对实际应用也是实用的。这不是另一种只在实验室工作的技术。Ajax应用可以是任意大小,从非常简单、功能单一的Google Suggest到非常复杂和精密的Google Maps。

在Adaptive Path,在过去的几个月中我们已经使用Ajax完成了我们自己的工作,并且我们认识到我们仅仅触及到Ajax应用可以提供的丰富交互和响应能力的一点皮毛。Ajax是Web应用的一个重要的发展,并且它的重要性只会继续增长。因为外面有那么多的开发者已经知道如何使用这些技术,我们期望看到更多的组织继Google之后率先获得Ajax提供的竞争优势。

继续前行

创建Ajax应用最大的挑战不是技术。核心Ajax技术是成熟的、稳定的、并且容易理解的。相反,挑战来自于这些应用的设计者:忘记我们所认为我们知道的关于Web的限制,并且开始想象一个更广泛、更丰富的可能性的范围。

那一定会很有趣。

Ajax Q&A

2005年3月13日: 自我们第一次发布Jesse的论文以来,我们收到了大量读者关于Ajax问题的信件。在这个Q&A中,Jesse回答了一些最常见的问题。

Q. 是Adaptive Path发明的Ajax吗?还是Google?是不是Adaptive Path帮助构建了Google的Ajax应用?

A. 既不是Adaptive Path也不是Google发明的Ajax。Google近期的产品仅仅是级别最高的Ajax应用例子。Adaptive Path没有参与Google的Ajax应用的开发,但我们一直在为我们其它的一些客户做Ajax的工作。

Q. Adaptive Path正在出售Ajax组件或注册这个名字的商标吗?我从哪里可以下载它?

A. Ajax不是你能下载的东西。它是一种方法——思考使用某种技术的Web应用架构的一种方式。Ajax这个名字和方法均不是Adaptive Path专有的。

Q. Ajax仅仅是XMLHttpRequest的另一个名字吗?

A. 不是。XMLHttpRequest只是Ajax方程式的一部分。XMLHttpRequest是使异步服务器通讯成为可能的技术组件;Ajax是我们给文章中描述的整体方法的名字,它不仅依赖于XMLHttpRequest,还依赖于CSS、DOM和其它技术。

Q. 为什么你觉得有必要给出这个名字?

A. 当和客户讨论这个方法时,我需要使用比“异步JavaScript + CSS + DOM + XMLHttpRequest”更短的东西。

Q. 异步服务器通讯技术已经存在好多年了。是什么让Ajax成为一种“新”方法的?

A. 新情况是这些技术在实际应用中的突出使用改变了Web的基本交互模型。Ajax现在大行其道是因为这些技术和行业对如何更有效地部署它们的理解已经经过了时间的发展。

Q. Ajax是一个技术平台或一种架构风格吗?

A. 都是。Ajax是以某种特殊方式同时使用的一组技术。

Q. Ajax最适合什么类型的应用?

A. 目前我们还不知道。因为它是一种相对较新的方法,我们对Ajax最适用在哪些地方的理解仍处于初级阶段。有时候传统的Web应用模型是某个问题的最合适的解决方案。

Q. 这意味着Adaptive Path是反Flash的吗?

A. 一点也不。Macromedia是Adaptive Path的一个客户,而且我们是Flash技术长期以来的支持者。随着Ajax的成熟,我们希望有时候Ajax是特定问题的最好的解决方案,而有时候Flash 是最好的解决方案。我们也有兴趣探索技术可以混合的方式(正如Flickr的案例,它使用了两者)。

Q. Ajax有显著的可访问性或浏览器兼容性限制吗?Ajax应用会破坏后退按钮吗?Ajax兼容REST吗?Ajax开发时是否有安全考虑?Ajax应用可以为那些已经关闭JavaScript的用户工作吗?

A. 所有这些问题的答案是“可能”。许多开发者已经在致力于解决这些问题。要确定Ajax所有的局限性,我们认为有更多的工作要做,并且我们期望Ajax开发社区一起来发现更多这类问题。

Q. 一些你举出的Google例子根本没有使用XML。我必须在Ajax应用中使用XML和/或XSLT吗?

A. 不。XML是最全面发展的在Ajax客户端内外获取数据的手段,但没有理由使用JavaScript Object Notation之类的技术或任何类似的结构化数据的手段为交换不能达到同样的效果。

Q. Ajax应用比传统Web应用更易于开发吗?

A. 不一定。Ajax应用必然涉及到在客户端运行复杂的JavaScript代码。让复杂的代码高效并且无缺陷不是一个掉以轻心的任务,需要更好的开发工具和框架来帮助我们面对挑战。

Q. Ajax应用经常交付比传统Web应用更好的体验吗?

A. 不一定。Ajax给交互设计师更多的灵活性。然而,我们拥有的能力越大,在运用它的时候我们必须使用的更谨慎。我们必须小心地使用Ajax来增强我们应用的用户体验,而不是削弱它。

这篇文章被来自Webhostinggeeks.com的Jovana Milutinovich翻译成了Serbo-Croatian语言。

在iBatis中如何将JavaBean的boolean值映射成数据库的CHAR(Y或N)值

Spring 1.2.8 + iBatis 2.1.5

JavaBean的boolean值映射到数据库的默认值是0和1,不是非常直观,现在希望能将boolean值映射成为Y和N。iBatis 2.0.5以后的版本提供了TypeHandlerCallback,可以用来解决这个问题。

首先定义数据表:

1
2
3
4
5
6
CREATE TABLE users (
    username varchar(40),
    password varchar(255),
    enabled char(1),
    PRIMARY KEY (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf-8;

YesNoTypeHandlerCallback实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class YesNoTypeHandlerCallback implements TypeHandlerCallback {

    public void setParameter(ParameterSetter setter, Object parameter) throws SQLException {
        if (parameter == null) {
            setter.setString("");
        } else {
            Boolean b = (Boolean)parameter;
            if (b.booleanValue()) {
                setter.setString("Y");
            } else {
                setter.setString("N");
            }
        }
    }

    public Object getResult(ResultGetter getter) throws SQLException {
        String string = getter.getString();
        if (string == null) {
            return null;
        } else if ("Y".equalsIgnoreCase(string)) {
            return new Boolean(true);
        } else {
            return new Boolean(false);
        }
    }

    public Object valueOf(String s) {
        return s;
    }
}

配置文件sql-map-config.xml中的内容如下:

1
2
3
4
5
6
<sqlMapConfig>
    <typeHandler jdbcType="CHAR" javaType="boolean"
                 callback="com.codemany.netlink.dao.support.YesNoTypeHandlerCallback" />

    <sqlMap resource="com/codemany/netlink/dao/impl/User.xml" />
</sqlMapConfig>

jdbcType="CHAR" javaType="boolean"必须有,否则YesNoTypeHandlerCallback.setParameter()不会被调用。

SqlMap映射文件User.xml部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<typeAlias alias="user" type="com.codemany.netlink.model.User" />

<resultMap id="userResult" class="user">
    <result property="username" column="username" />
    <result property="password" column="password" />
    <result property="enabled" column="enabled" jdbcType="CHAR" javaType="boolean" />
</resultMap>

<select id="getUser" parameterClass="string" resultMap="userResult">
<![CDATA[
    SELECT username, password, enabled
    FROM user WHERE username = #username#
]]>
</select>

<insert id="addUser" parameterClass="user">
<![CDATA[
    INSERT INTO user (username, password, enabled)
    VALUES (#username#, #password#, #enabled,jdbcType=CHAR,javaType=boolean#)
]]>
</insert>

jdbcType="CHAR" javaType="boolean"必须有,否则YesNoTypeHandlerCallback.getResult()不会被调用;#enabled,jdbcType=CHAR,javaType=boolean#也必须这样写,否则YesNoTypeHandlerCallback.setParameter()也不会被调用,写成#enabled:CHAR#或#enabled#都不起作用。

DbUnit结合Spring进行SqlMap的单元测试

使用DbUnit,开发者可以控制测试数据库的状态。进行一个DAO单元测试之前,DbUnit为数据库准备好初始化数据;而在测试结束时,DbUnit会把数据库状态恢复到测试前的状态。下面的例子使用DbUnit为SqlMap编写单元测试。

SqlMap映射文件User.xml的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<select id="getUser" parameterClass="String" resultClass="User">
    SELECT username, password, first_name, last_name
    FROM user WHERE username = #username#
</select>

<delete id="removeUser" parameterClass="String">
    DELETE FROM user WHERE username = #username#
</delete>

<update id="updateUser" parameterClass="User">
    UPDATE user SET
        password = #password#,
        first_name = #firstName#,
        last_name = #lastName#
    WHERE username =#username#
</update>

<insert id="insertUser" parameterClass="User">
    INSERT INTO user (username, password, first_name, last_name)
    VALUES (#username#, #password#, #firstName#, #lastName#)
</insert>

配置文件sql-map-config.xml中的内容如下:

1
2
3
4
5
6
7
8
9
10
<sqlMapConfig>
    <settings lazyLoadingEnabled="true"
              cacheModelsEnabled="true"
              enhancementEnabled="true"
              maxSessions="64"
              maxTransactions="8"
              maxRequests="128" />

    <sqlMap resource="com/codemany/netlink/dao/impl/User.xml" />
</sqlMapConfig>

首先,要为单元测试准备测试数据。我们可以用DbUnit的Flat XML格式来准备测试数据集。下面的XML文件称为目标数据库的Seed File,它为测试准备了两条数据。其中元素名user对应数据库的表名,属性username,password,first_name和last_name是表user的列名,属性值就是记录值。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<dataset>
    <user username="ford"
        password="ford"
        first_name="Henry"
        last_name="Ford" />

    <user username="twain"
        password="twain"
        first_name="Mark"
        last_name="twain" />
</dataset>

缺省情况下,DbUnit在单元测试开始之前执行CLEAN_INSERT操作,删除Seed File中所有表的数据,并导入Seed File的测试数据。我们可以通过覆盖getSetUpOperation()和getTearDownOperation()方法来控制单元测试前和测试后的数据库状态。一种高效的实施方案就是让getSetUpOperation()方法执行REFRESH操作,通过执行这个操作,我们可以用Seed File中的数据去更新目标数据库里的数据。接下来,就是getTearDownOperation(),让他执行一个NONE操作,也就是什么也不执行。

1
2
3
4
5
6
7
protected DatabaseOperation getSetUpOperation() throws Exception {
    return DatabaseOperation.REFRESH;
}

protected DatabaseOperation getTearDownOperation() throws Exception {
    return DatabaseOperation.NONE;
}

为了方便测试,我们为SqlMap的单元测试编写一个抽象的测试基类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public abstract class SqlMapTestCase extends DatabaseTestCase {
    protected SqlMapClient sqlMapClient = null;
    protected Properties props = new Properties();

    protected IDatabaseConnection getConnection() throws Exception {
        props.load(Resources.getResourceAsStream("properties/database.properties"));

        Class.forName(props.getProperty("driver"));
        Connection conn = DriverManager.getConnection(props.getProperty("url"),
                props.getProperty("username"), props.getProperty("password"));
        return new DatabaseConnection(conn);
    }

    protected IDataSet getDataSet() throws Exception {
        String resource = "com/codemany/netlink/dao/impl/dataset.xml";
        return new FlatXmlDataSet(Resources.getResourceAsStream(resource));
    }

    protected void setUp() throws Exception {
        super.setUp();

        // Build the SqlMapClient
        Reader reader = Resources.getResourceAsReader("sql-map-config.xml");
        sqlMapClient = SqlMapClientBuilder.buildSqlMapClient(reader);

        // Tell the SqlMapClient to use the given DataSource
        DataSource dataSource = getDataSource();
        TransactionConfig transactionConfig = getTransactionConfig(dataSource);
        // Apply the given TransactionConfig to the SqlMapClient
        applyTransactionConfig(sqlMapClient, transactionConfig);
    }

    protected void tearDown() throws Exception {
        super.tearDown();

        if (sqlMapClient != null) {
            DataSource ds = sqlMapClient.getDataSource();
            Connection conn = ds.getConnection();
            conn.close();
        }
    }

    private DataSource getDataSource() throws Exception {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(props.getProperty("driver"));
        dataSource.setUrl(props.getProperty("url"));
        dataSource.setUsername(props.getProperty("username"));
        dataSource.setPassword(props.getProperty("password"));
        return dataSource;
    }

    private TransactionConfig getTransactionConfig(DataSource dataSource) throws Exception {
        Properties transactionConfigProperties = new Properties();
        transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");

        TransactionConfig transactionConfig = (TransactionConfig)ExternalTransactionConfig.class.newInstance();
        transactionConfig.setDataSource(dataSource);
        transactionConfig.initialize(transactionConfigProperties);
        return transactionConfig;
    }

    private void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
        if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
            throw new IllegalArgumentException("Cannot set TransactionConfig with DataSource"
                    + "for SqlMapClient if not of type ExtendedSqlMapClient: " + sqlMapClient);
        }
        ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient)sqlMapClient;
        transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate().getMaxTransactions());
        extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
    }
}

然后为每个SqlMap映射文件编写一个测试类,继承上面的抽象类:

1
2
3
4
5
6
7
8
public class UserSqlMapTest extends SqlMapTestCase {

    public void testGetUser() throws Exception {
        User user = (User)sqlMapClient.queryForObject("getUser", "ford");
        assertNotNull(user);
        assertEquals("ford", user.getName());
    }
}

如此就可以进行单元测试了。

如何在WebWork的资源文件中获取验证字段的值

WebWork是一个非常不错的MVC框架,有着众多的优点,也有着细微的缺陷。在实现一个用户注册验证的例子时,发现在资源文件中不能获取验证字段的值,只能每个字段一个message。

1
2
3
class User {
    private String username;
    private String password;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<field name="username">
    <field-validator type="stringlength" short-circuit="true">
        <param name="minLength">5</param>
        <param name="maxLength">15</param>
        <message key="errors.username.range" />
    </field-validator>
</field>

<field name="password">
    <field-validator type="stringlength" short-circuit="true">
        <param name="minLength">5</param>
        <param name="maxLength">15</param>
        <message key="errors.password.range" />
    </field-validator>
</field>

资源文件中的消息内容:

1
2
errors.username.range=${username} is not in the range ${minLength} through ${maxLength}.
errors.password.range=${password} is not in the range ${minLength} through ${maxLength}.

到xwork的站点上去查找解决方法,在Issue Tracker里看到有人提交了一个Feature,采用的办法是给message传递参数,看了xwork的代码后,发现这样子的做法要修改的代码量非常大,所以只能另外想办法解决。

在资源文件中可以用${fieldName}来获取要验证的字段变量名,那是否可以用${fieldValue}来得到验证字段的值呢?查看代码后发现这种办法行不通。那么是否可以用${${fieldName}}来获得字段变量的值呢?查看代码发现验证失败后会调用Validator.getMessage()方法,追踪代码来到TextParseUtil.translateVariables()方法中,找到代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (start != -1 && x < length && count != 0) {    // 该部分代码负责查找变量,即${}包围的字符串
    c = expression.charAt(x++);
    if (c == '{') {
        count++;
    } else if (c == '}') {
        count--;
    }
}
end = x - 1;

if ((start != -1) && (end != -1) && (count == 0)) {
    String var = expression.substring(start + 2, end);

    Object o = stack.findValue(var, asType);    // 此处就是获取变量值的地方

    String left = expression.substring(0, start);
    String right = expression.substring(end + 1);

由上面的代码可以看出WebWork只处理了最外围的${},所以要让它处理像${${}}这样的格式就需要在获取变量前再次处理变量。修改后的代码如下:

1
2
3
4
5
6
7
8
9
if ((start != -1) && (end != -1) && (count == 0)) {
    String nestedExpression = expression.substring(start + 2, end);
    String var = translateVariables(nestedExpression, stack);
    //String var = expression.substring(start + 2, end);

    Object o = stack.findValue(var, asType);

    String left = expression.substring(0, start);
    String right = expression.substring(end + 1);

现在就可以处理资源文件中的多重嵌套变量了。接着把修改验证文件和资源文件修改一下就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<field name="username">
    <field-validator type="stringlength" short-circuit="true">
        <param name="minLength">5</param>
        <param name="maxLength">15</param>
        <message key="errors.range" />
    </field-validator>
</field>

<field name="password">
    <field-validator type="stringlength" short-circuit="true">
        <param name="minLength">5</param>
        <param name="maxLength">15</param>
        <message key="errors.range" />
    </field-validator>
</field>
1
errors.range=${${fieldName}} is not in the range ${minLength} through ${maxLength}.

如何去除WinRAR 3.30的注册提示框

WinRAR在使用到期后,每次打开都会弹出如下图所示的对话框窗口,十分烦人。

去除Nag窗口常用的几种方法是:

  1. 使用资源修改工具将可执行文件中的Nag窗口的属性改成透明、不可见,可以变相的去除Nag窗口;
  2. 找到创建和显示Nag窗口的代码,跳过即可。显示窗口的常用函数有MessageBox、MessageBoxEx、DialogBoxParam、CreateWindowEx、CreateWindowEx、ShowWindow等;
  3. 找到创建和显示Nag窗口的函数,更改其参数,让其调用失败就可以了;
  4. 在Nag窗口弹出来后,给可以关闭它的BUTTON 送一个WM_COMMAND消息;
  5. 通过静态和动态分析找到注册码。

由于WinRAR未注册版本没有功能限制,用不着去找注册码注册。所以在这里可以用2和3两种方法去掉WinRAR的注册提示窗口。

使用Resource Hacker打开WinRAR.exe,显示Nag窗口的资源如图所示:

Nag窗口的ID号是1049,换算成16进制是0x419。用W32Dasm打开WinRAR.exe,打开Dialog References查找Dialog: DialogID_0419,结果没找到。由上图可知Nag窗口的资源名是REMINDER,打开String Data References查找REMINDER字符串试试,果然找到了,双击该项就可以来到相关代码处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0043FE89(C)
|
:0043FE8F C6055037490001          mov byte ptr [00493750], 01
:0043FE96 6A00                    push 00000000
:0043FE98 68143F4400              push 00443F14
:0043FE9D FF3588EC4A00            push dword ptr [004AEC88]

* Possible StringData Ref from Data Obj ->"REMINDER"
|
:0043FEA3 68C93C4900              push 00493CC9
:0043FEA8 FF3560D04900            push dword ptr [0049D060]

* Reference To: USER32.DialogBoxParamA, Ord:0000h
|
:0043FEAE E837E30400              Call 0048E1EA

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:0043FE64(C), :0043FE6D(C), :0043FE76(C), :0043FE7F(C), :0043FE8D(C)
|
:0043FEB3 833DF8104B0000          cmp dword ptr [004B10F8], 00000000
:0043FEBA 752D                    jne 0043FEE9
:0043FEBC 833DF0104B0000          cmp dword ptr [004B10F0], 00000000
:0043FEC3 7524                    jne 0043FEE9
:0043FEC5 833D08114B00FF          cmp dword ptr [004B1108], FFFFFFFF
:0043FECC 741B                    je 0043FEE9
:0043FECE 6A0A                    push 0000000A
:0043FED0 FF3508114B00            push dword ptr [004B1108]

可以看到,REMINDER字符串地址是作为参数传给了DialogBoxParamA函数,而DialogBoxParamA函数正是创建对话框的API函数。这时只需将0043FEA3处的push 00493CC9改为push 0,将一个NULL传入DialogBoxParamA函数,这个函数就会调用失败。当然之后它也就不能正常显示了,就达到了去除的目的。

现在再打开WinRAR,呵呵,大功告成,再也不会有Nag窗口出来Nag我了。

让Struts-Menu能访问WebWork的ResourceBundle

Struts-Menu默认是通过JSTL或者Struts来读取ResourceBundle的,这就需要在web.xml中配置要读取的properties文件(JSTL方式)或在struts-config.xml中配置(Struts方式)。 WebWork也有自己的ResourceBundle配置方式,而且Struts-Menu也不支持访问WebWork的ResourceBundle的功能。所以参考着DisplayTag的实现对Struts-Menu进行了一番手术,实现了可访问WebWork的ResourceBundle的功能。

下面是修改步骤:

  • 在Struts-Menu项目中增加net/sf/navigator/localization目录,将org.displaytag.localization中的*.java移到该目录中;
  • 将org.displaytag下的Message.java和messages.properties移到net/sf/navigator目录下;
  • 给MenuDisplayerMapping添加两个属性localeResover和ResourceProvider;
  • 在struts-menu.xml的Displayer标签下添加:
1
2
<setProperty property="localeResover" value="net.sf.navigator.localization.I18nWebworkAdapter" />
<setProperty property="resourceProvider" value="net.sf.navigator.localization.I18nWebworkAdapter" />
  • 在MenuDisplayer中添加以下四个接口方法,并在AbstractMenuDisplayer里实现它们:
1
2
3
4
public LocaleResolver getLocaleResolver();
public void setLocaleResolver(LocaleResolver localeResolver);
public I18nResourceProvider getResourceProvider();
public void setResourceProvider(I18nResourceProvider resourceProvider);
  • 在AbstractMenuDisplayer里添加protected的PageContext对象,并在init方法里初始化它;
  • 覆写MessageResourcesMenuDisplayer.getMessage方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
public String getMessage(String key) {
    String message = null;

    if (resourceProvider != null) {
        message = resourceProvider.getResource(key, "???", null, pageContext);
    }
    if (message == null) {
        message = key;
    }

    return message;
}
  • 在UseMenuDisplayerTag文件里添加代码(+后的代码是增加的):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  // get an instance of the menu displayer
  MenuDisplayer displayerInstance = null;
+ LocaleResolver localeResolver = null;
+ I18nResourceProvider resourceProvider = null;

  // default to use the config on the mapping
  if (displayerMapping.getConfig() != null) {
      // this value (config) is set on the displayer below
      setConfig(displayerMapping.getConfig());
  }
+ localeResolver =
+         (LocaleResolver) Class.forName(displayerMapping.getLocaleResover()).newInstance();
+ resourceProvider =
+         (I18nResourceProvider) Class.forName(displayerMapping.getLocaleProvider()).newInstance();

  displayerInstance.setConfig(config);
+ displayerInstance.setResourceProvider(resourceProvider);
+ displayerInstance.setLocaleResolver(localeResolver);