乐者为王

Do one thing, and do it well.

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在使用到期后,每次打开都会弹出如下图所示的对话框窗口,十分烦人。

winrar-nag

去除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窗口的资源如图所示:

winrar-reminder

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

winrar-w32dasm

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);

Ant系列之用EMMA测量测试覆盖率的问题

EMMA是一个coverage工具,有on-the-fly和offline两种使用方式,在Ant里通常采用offline方式。

到sourceforge下载了2.0版本的EMMA下来,由于将EMMA的生成文件做了重定向,因此遇到了些问题,比如出现以下错误:

1
nothing to do: no runtime coverage data found in any of the data files

查找EMMA的FAQ 3.7节,只是告诉你可能少了metadata和runtime coverate date(对Ant用户来说一般是少了runtime coveraget date,它的后缀名默认是ec)。

由FAQ 3.15节可以知道runtime coverage data文件默认是生成在user.dir目录下的(即build.xml中的basedir目录下),而由instr指令生成的metadata文件(后缀名默认为em)则已经被重定向到另外的目录(用来生成coverage report的地方),所以必须重新设置coverage.out.file的值,用来重定向生成的runtime coverage data。因为runtime coverage data是在junit任务执行时产生的,所以我们可以在junit任务中添加一行代码:

1
<sysproperty key="emma.coverage.out.file" value="${coverage.dir}/coverage.emma" />

注意:

  1. metadata和runtime文件的后缀名最好设置相同;
  2. 一定要将junit任务的fork属性设置为true,这是因为EMMA 2.0的runtime coverage data是在JVM退出后生成的;
  3. instrumented classes必须是第一个被JVM执行的,所以instrumented classes目录必须处在junit任务的classpath的第一行,具体原因不明,如果谁知道还请告知下。

其实EMMA附带的examples目录下的build-offline.xml里已经说明的很详细了,只不过下载后没有仔细看,才遇到了一些问题。下面是build.xml的部分代码:

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
<path id="classpath">
  <pathelement location="${instrumentedclasses}" />
  <pathelement location="${classes}" />
  <pathelement location="${testclasses}" />
  <fileset dir="${lib}" includes="**/*.jar" />
</path>

<target name="compile" depends="prepare">
  <javac srcdir="${src}" destdir="${classes}" debug="on">
    <classpath refid="classpath" />
  </javac>
  <javac srcdir="${test}" destdir="${testclasses}" debug="on">
    <classpath refid="classpath" />
  </javac>
</target>

<target name="coverage.instrument" depends="compile">
  <emma enabled="yes">
    <instr instrpath="${classes}" destdir="${instrumentedclasses}"
        metadatafile="{coveragereports}/metadata.emma" merge="true">
    </instr>
  </emma>
</target>

<target name="junit" depends="coverage.instrument">
  <junit printsummary="yes" haltonfailure="no" fork="true">
    <sysproperty key="emma.coverage.out.file" value="${coveragereports}/coverage.emma" />
    <classpath refid="classpath" />

    <formatter type="xml" />

    <batchtest fork="yes" todir="${junitreports}">
      <fileset dir="${test}">
        <include name="**/*Test*.java" />
      </fileset>
    </batchtest>
  </junit>
</target>

<target name="coverage.report" depends="test">
  <emma enabled="yes">
    <report sourcepath="${src}">
      <fileset dir="${coveragereports}">
        <include name="*.emma" />
      </fileset>

      <html outfile="${coveragereports}/coverage.html" depth="method" />
    </report>
  </emma>
</target>

修复CheckStyle报告中Summary部分文件数目错误问题

在使用CheckStyle时用了它提供的checkstyle-frames.xsl作为样式文件,不过checkstyle-frames.xsl生成报告时有些问题,主要是统计数据有点问题,Summary中报告的文件数目比真实的多,所以对xsl文件的summary模板作了小小的一些修改,解决了这个问题,且添加了一个统计有错误的文件个数的功能。

原来的xsl内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<xsl:template match="checkstyle" mode="summary">
    <h3>Summary</h3>
    <xsl:variable name="fileCount" select="count(file)"/>
    <xsl:variable name="errorCount" select="count(file/error)"/>
    <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
        <tr>
            <th>Files</th>
            <th>Errors</th>
        </tr>
        <tr>
            <xsl:call-template name="alternated-row"/>
            <td><xsl:value-of select="$fileCount"/></td>
            <td><xsl:value-of select="$errorCount"/></td>
        </tr>
    </table>
</xsl:template>

修改后的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<xsl:template match="checkstyle" mode="summary">
    <h3>Summary</h3>
    <xsl:variable name="fileCount">
        <xsl:value-of select="count(file[not(./@name = preceding-sibling::file/@name)])" />
    </xsl:variable>
    <xsl:variable name="fileErrorCount" select="count(file[count(error) > 0])" />
    <xsl:variable name="errorCount" select="count(file/error)" />
    <table class="log" border="0" cellpadding="5" cellspacing="2" width="100%">
        <tr>
            <th>Total Files</th>
            <th>Files With Errors</th>
            <th>Errors</th>
        </tr>
        <tr>
            <xsl:call-template name="alternated-row" />
            <td><xsl:value-of select="$fileCount" /></td>
            <td><xsl:value-of select="$fileErrorCount" /></td>
            <td><xsl:value-of select="$errorCount" /></td>
        </tr>
    </table>
</xsl:template>
  • following-sibling 按文档顺序选择文档中此后出现的当前节点的所有兄弟节点。
  • preceding-silbling 按与文档顺序相反的方向选择文档中此前出现的当前节点的所有兄弟节点。
  • following 除当前节点的所有后代节点外,按顺序选择文档中当前节点之后出现的所有节点,不包括属性节点或名称空间节点。
  • preceding 按与文档方向顺序相反的方向选择文档中在当前节点之前出现的所有节点。

Ant系列之用JDepend生成包依赖性度量的问题

首先获取JDepend的最新jar文件jdepend-2.9.jar,把它添加到ANT_HOME/lib目录里。

在build.xml中添加:

1
2
3
4
5
6
7
<target name="jdepend" description="Generate dependency metrics for each package">
    <jdepend format="xml" outputfile="${jdepend.dir}/jdepend-report.xml">
        <classpath>
            <pathelement path="classes" />
        </classpath>
    </jdepend>
</target>

运行任务后出现如下的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BUILD FAILED
build.xml:59: Missing classespath required argument
    at org.apache.tools.ant.taskdefs.optional.jdepend.JDependTask.execute(JDependTask.java:397)
    at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:275)
    at org.apache.tools.ant.Task.perform(Task.java:364)
    at org.apache.tools.ant.Target.execute(Target.java:341)
    at org.apache.tools.ant.Target.performTasks(Target.java:369)
    at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1216)
    at org.apache.tools.ant.Project.executeTarget(Project.java:1185)
    at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:40)
    at org.eclipse.ant.internal.ui.antsupport.EclipseDefaultExecutor.executeTargets(EclipseDefaultExecutor.java:32)
    at org.apache.tools.ant.Project.executeTargets(Project.java:1068)
    at org.eclipse.ant.internal.ui.antsupport.InternalAntRunner.run(InternalAntRunner.java:423)
    at org.eclipse.ant.internal.ui.antsupport.InternalAntRunner.main(InternalAntRunner.java:137)

查看JDependTask.java文件的397行:

1
2
3
4
5
6
if (getSourcespath() == null && getClassespath() == null) {
    throw new BuildException("Missing classespath required argument");
} else if (getClassespath() == null) {
    String msg = "sourcespath is deprecated in JDepend >= 2.5 - please convert to classespath";
    log(msg);
}

发现是getClassespath() == null了,跳到221行:

1
2
3
public Path getClassespath() {
    return classesPath;
}

不是写了classpath吗?怎么classesPath的值会等于null呢?结果发现在247行有:

1
2
3
4
5
6
7
public void setClasspath(Path classpath) {
    if (compileClasspath == null) {
        compileClasspath = classpath;
    } else {
        compileClasspath.append(classpath);
    }
}

原来是在build.xml中把classespath错写成classpath了,那样当然有问题啦。改过来,再运行就可以了。

Ant系列之用Checkstyle检查代码规范的问题

首先将checkstyle-all-4.1.jar文件拷贝到项目的lib目录下,并建立CheckStyle配置文件。

然后在build.xml文件中声明CheckStyle任务:

1
<taskdef resource="checkstyletask.properties" classpath="${lib.dir}/checkstyle-all-4.1.jar" />

接着是建立CheckStyle任务:

1
2
3
4
5
6
7
8
9
10
<target name="checkstyle" description="Generates a report of code convention violations.">
    <checkstyle config="${docs.dir}/checkstyle_checks.xml" failOnViolation="false">
        <fileset dir="${src.dir}" includes="**/*.java" />
        <formatter type="xml" toFile="${checkstyle.dir}/checkstyle_report.xml" />
    </checkstyle>
    <xslt basedir="${checkstyle.dir}" destdir="${checkstyle.dir}/html" extension=".html"
            style="${docs.dir}/checkstyle-frames.xsl">
        <param name="output.dir" expression="${checkstyle.dir}/html" />
    </xslt>
</target>

其中output.dir是checkstyle-frames.xsl中的参数:

1
<xsl:param name="output.dir" />

在运行任务时可能会出现如下异常:

1
javax.xml.transform.TransformerException: java.lang.RuntimeException: Unrecognized XSLTC extension 'org.apache.xalan.xslt.extensions.Redirect:write'

可以将checkstyle-frames.xsl中的:

1
xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect"

改成下面的内容:

1
xmlns:redirect="http://xml.apache.org/xalan/redirect"

如果出现这样的异常:

1
Got an exception - java.lang.RuntimeException: Unable to get class information for

可以修改对抛出异常的限制:允许抛出非检查型异常异常和抛出另一个已声明异常的子类。

1
2
3
4
<module name="RedundantThrows">
    <property name="allowUnchecked" value="true" />
    <property name="allowSubclasses" value="true" />
</module>

WebWork中如何实现i18n

ognl-2.6.5.jar + oscore-2.2.4.jar + webwork-2.1.7.jar + xwork-1.0.5.jar

web.xml配置文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <display-name>i18n</display-name>

    <servlet>
        <servlet-name>action</servlet-name>
        <servlet-class>com.opensymphony.webwork.dispatcher.ServletDispatcher</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>

    <taglib>
        <taglib-uri>webwork</taglib-uri>
        <taglib-location>/WEB-INF/webwork.tld</taglib-location>
    </taglib>
</web-app>

webwork.properties配置文件:

1
2
3
4
5
6
7
### This can be used to set your locale and encoding scheme
#webwork.locale=en_US    <- 这行一定要注释掉,因为WebWork首先判断webwork.locale有没有被设置,
                            如有则始终以该locale为准,忽略浏览器的locale
webwork.i18n.encoding=utf-8

### Load custom default resource bundles
webwork.custom.i18n.resources=ApplicationResources

xwork.xml配置文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xwork PUBLIC
    "-//OpenSymphony Group//XWork 1.0//EN"
    "http://www.opensymphony.com/xwork/xwork-1.0.dtd">

<xwork>
    <include file="webwork-default.xml" />

    <package name="default" extends="webwork-default">
        <action name="hello" class="com.codemany.i18n.action.HelloAction">
            <result name="success" type="dispatcher">/i18n.jsp</result>
        </action>
    </package>
</xwork>

HelloAction.java实现代码:

1
2
3
4
5
6
7
8
import com.opensymphony.xwork.ActionSupport;

public class HelloAction extends ActionSupport {

    public String execute() throws Exception {
        return SUCCESS;
    }
}

i18n.jsp的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page pageEncoding="utf-8" contentType="text/html; charset=utf-8" %>

<%@ taglib uri="webwork" prefix="ww" %>

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>

<body>
    <ww:property value="getText('i18n.value')" />
</body>
</html>

ApplicationResources_zh_CN.properties的内容:

1
i18n.value=国际化

ApplicationResources_en_US.properties的内容:

1
i18n.value=internationalization

练习破解'Crackme2 - by CoSH'

Crackme2程序

  1. 用PEiD查看,程序没有加壳;
  2. 首先找到注册错误提示信息“One of the Details you entered was wrong”;
  3. 用W32Dasm反汇编,利用String Data References找到上述字符串,双击它,看到以下程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
* Reference To: MFC42.Ordinal:0F24, Ord:0F24h
|
:004014EB E85A030000              call 0040184A
:004014F0 83F805                  cmp eax, 00000005                 -> 比较Name的长度是否不大于5
:004014F3 7E41                    jle 00401536                      -> 如果是就跳到出错信息处
:004014F5 8D86E0000000            lea eax, dword ptr [esi+000000E0] -> Name字符串的地址
:004014FB 8BCF                    mov ecx, edi
:004014FD 50                      push eax

* Reference To: MFC42.Ordinal:0F22, Ord:0F22h
|
:004014FE E841030000              call 00401844
:00401503 8DBEE4000000            lea edi, dword ptr [esi+000000E4] -> Serial字符串的地址
:00401509 8BCD                    mov ecx, ebp
:0040150B 57                      push edi

从004014F0到004014F3可以知道Name必须大于5个字符,且和Serial无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
* Reference To: MFC42.Ordinal:0F22, Ord:0F22h
|
:0040150C E833030000              call 00401844
:00401511 8B07                    mov eax, dword ptr [edi]
:00401513 803836                  cmp byte ptr [eax], 36
:00401516 751E                    jne 00401536                      -> 跳到出错信息处
:00401518 80780132                cmp byte ptr [eax+01], 32
:0040151C 7518                    jne 00401536                      -> 跳到出错信息处
:0040151E 80780238                cmp byte ptr [eax+02], 38
:00401522 7512                    jne 00401536                      -> 跳到出错信息处
:00401524 80780337                cmp byte ptr [eax+03], 37
:00401528 750C                    jne 00401536                      -> 跳到出错信息处
:0040152A 8078042D                cmp byte ptr [eax+04], 2D
:0040152E 7506                    jne 00401536                      -> 跳到出错信息处
:00401530 80780541                cmp byte ptr [eax+05], 41
:00401534 7417                    je 0040154D                       -> 跳到正确信息处

由上面的比较代码可以得到:

1
2
3
4
5
6
36(hex) = 6
32(hex) = 2
38(hex) = 8
37(hex) = 7
2D(hex) = -
41(hex) = A

所以Serial是:6287-A

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
* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:004014E4(C), :004014F3(C), :00401516(C), :0040151C(C), :00401522(C)
|:00401528(C), :0040152E(C)
|
:00401536 6A00                    push 00000000

* Possible StringData Ref from Data Obj ->"ERROR"
|
:00401538 6864304000              push 00403064

* Possible StringData Ref from Data Obj ->"One of the Details you entered was wrong"
|
:0040153D 6838304000              push 00403038
:00401542 8BCE                    mov ecx, esi

* Reference To: MFC42.Ordinal:1080, Ord:1080h
|
:00401544 E8F5020000              Call 0040183E
:00401549 6A00                    push 00000000
:0040154B FFD3                    call ebx

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00401534(C)
|
:0040154D 8D8EE0000000            lea ecx, dword ptr [esi+000000E0]
:00401553 8D542414                lea edx, dword ptr [esp+14]
:00401557 51                      push ecx

* Possible StringData Ref from Data Obj ->"Well done,"
|
:00401558 682C304000              push 0040302C
:0040155D 52                      push edx

* Reference To: MFC42.Ordinal:039E, Ord:039Eh
|
:0040155E E8D5020000              Call 00401838
:00401563 683C314000              push 0040313C
:00401568 50                      push eax
:00401569 8D442418                lea eax, dword ptr [esp+18]
:0040156D C744242800000000        mov [esp+28], 00000000
:00401575 50                      push eax

* Reference To: MFC42.Ordinal:039C, Ord:039Ch
|
:00401576 E8B7020000              Call 00401832
:0040157B 8B00                    mov eax, dword ptr [eax]
:0040157D 6A00                    push 00000000

* Possible StringData Ref from Data Obj ->"YOU DID IT"
|
:0040157F 6820304000              push 00403020
:00401584 50                      push eax
:00401585 8BCE                    mov ecx, esi
:00401587 C644242C01              mov [esp+2C], 01

整理后得到:Name的长度必须大于5个字符,且和Serial无关。

小技巧:如何截取W32Dasm中的汇编代码呢?很简单,在W32Dasm中点击其最左边,会有一红点,再按Shift键,点击另一处,选中所需范围,按Ctrl+C复制到剪贴版,剩下的事就是粘贴了。