Skip to content

14. 示例 11 – 日期转换与验证

新应用程序包含日期输入功能:

  • 在 [1] 中,输入表单
  • 转换为 [2],返回的响应

该应用程序的运作方式与整数输入类似,因此我们仅对不同之处进行说明。

14.1. NetBeans 项目

NetBeans 项目如下:

  • 在 [1] 中,应用程序的视图
  • [Accueil.jsp]:首页
  • [FormDate.jsp]:数据输入表单
  • [ConfirmationFormDate.jsp]:确认页面
  • 在 [2] 中,消息文件 [messages.properties] 以及 Struts 的主配置文件
  • 在 [3] 中:
  • [FormDate.java]:显示并处理表单的操作
  • [FormDate-validation.xml]:[FormDate] 动作的验证规则。该文件将这些验证任务委托给模型。
  • [FormDateModel]:操作 [FormDate] 的模型
  • [ FormDateModel-validation.xml]:模型的验证规则
  • [FormDateModel.properties]:模型的消息文件
  • [example.xml]:Struts 的辅助配置文件

14.2. 项目配置

该项目主要通过以下文件 [example.xml] 进行配置:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
        "http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
  <package name="example" namespace="/example" extends="struts-default">
    <action name="Accueil">
      <result name="success">/example/Accueil.jsp</result>
    </action>
    <action name="FormDate" class="example.FormDate">
      <result name="input">/example/FormDate.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormDate.jsp</result>
    </action>
  </package>
</struts>

它与之前研究过的整数输入配置文件类似。

14.3. 消息文件

文件 [messages.properties] 如下所示:


Accueil.titre=Accueil
Accueil.message=Struts 2 - Conversions et validations
Accueil.FormDate=Saisie de dates
Form.titre=Conversions et validations
FormDate.message=Struts 2 - Conversion et validation de dates
FormDate.conseil=Tapez les dates au format JJ/MM/AAAA comme dans 12/10/2008
Form.submitText=Valider
Form.cancelText=Annuler
Form.clearModel=Raz mod\u00e8le
Confirmation.titre=Confirmation
Confirmation.message=Confirmation des valeurs saisies
Confirmation.champ=champ
Confirmation.valeur=valeur
Confirmation.lien=Formulaire de test
xwork.default.invalid.fieldvalue=Valeur invalide pour le champ "{0}".

文件 [FormDateModel.properties] 如下:


date.format={0,date,dd/MM/yyyy}
date1.prompt=1-Tapez une date au format JJ/MM/AAAA
date1.error=Date erron\u00E9e
date2.prompt=2-Tapez une date au format JJ/MM/AAAA
date2.error=Date erron\u00E9e
date3.prompt=3-Tapez une date >=18/05/2000
date3.error=Date erron\u00E9e
date4.prompt=4-Tapez une date <=20/06/2001
date4.error=Date erron\u00E9e
date5.prompt=5-Tapez une date dans l''intervalle [18/05/2000,20/06/2001]
date5.error=Date errron\u00E9e
date6.prompt=6-Tapez une date dans l''intervalle [18/05/2000,20/06/2001]
date6.error=Date errron\u00E9e

第 1 行起着重要作用。我们稍后会再讨论这一点。

14.4. 输入表单

视图 [FormDate.jsp] 如下所示:


<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <title><s:text name="Form.titre"/></title>
    <s:head/>
  </head>

  <body background="<s:url value="/ressources/standard.jpg"/>">
    <h2><s:text name="FormDate.message"/></h2>
    <h4><s:text name="FormDate.conseil"/></h4>
    <s:form name="formulaire" action="FormDate">
      <s:textfield name="date1" key="date1.prompt"/>
      <s:textfield name="date2" key="date2.prompt" value="%{#parameters['date2']!=null ? #parameters['date2'] : date2==null ? '' :getText('date.format',{date2})}"/>
      <s:textfield name="date3" key="date3.prompt" value="%{#parameters['date3']!=null ? #parameters['date3'] : date3==null ? '' :getText('date.format',{date3})}"/>
      <s:textfield name="date4" key="date4.prompt" value="%{#parameters['date4']!=null ? #parameters['date4'] : date4==null ? '' :getText('date.format',{date4})}"/>
      <s:textfield name="date5" key="date5.prompt" value="%{#parameters['date5']!=null ? #parameters['date5'] : date5==null ? '' :getText('date.format',{date5})}"/>
      <s:textfield name="date6" key="date6.prompt"/>
      <s:submit key="Form.submitText" method="execute"/>
    </s:form>
    <br/>
    <s:url id="url" action="FormDate" method="cancel"/>
    <s:a href="%{url}"><s:text name="Form.cancelText"/></s:a>
      <br/>
    <s:url id="url" action="FormDate" method="clearModel"/>
    <s:a href="%{url}"><s:text name="Form.clearModel"/></s:a>
  </body>
</html>

第13至18行是六个日期输入区域。字段date2至date5具有一个复杂的value属性,与实数输入时的属性相同。 由于遇到了相同的困难,因此采用了相同的方法来解决。

14.5. 确认视图

确认视图 [ConfirmationFormDate.jsp] 如下所示:

Image

其代码如下:


<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
  <head>
    <title><s:text name="Confirmation.titre"/></title>
    <s:head/>
  </head>

  <body background="<s:url value="/ressources/standard.jpg"/>">
    <h2><s:text name="Confirmation.message"/></h2>
    <table border="1">
      <tr>
        <th><s:text name="Confirmation.champ"/></th>
        <th><s:text name="Confirmation.valeur"/></th>
      </tr>
      <tr>
        <td><s:text name="date1.prompt"/></td>
        <td><s:text name="date1"/></td>
      </tr>
      <tr>
        <td><s:text name="date2.prompt"/></td>
        <td>
          <s:text name="date.format">
            <s:param value="date2"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="date3.prompt"/></td>
        <td>
          <s:text name="date.format">
            <s:param value="date3"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="date4.prompt"/></td>
        <td>
          <s:text name="date.format">
            <s:param value="date4"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="date5.prompt"/></td>
        <td>
          <s:text name="date.format">
            <s:param value="date5"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="date6.prompt"/></td>
        <td><s:text name="date6"/></td>
      </tr>
    </table>
    <br/>
    <s:url id="url" action="FormDate!input"/>
    <s:a href="%{url}"><s:text name="Confirmation.lien"/></s:a>
  </body>
</html>

视图 [ConfirmationFormDate.jsp] 仅用于显示模板 [FormDateModel]。第 47-49 行展示了日期的显示效果。 我们希望该显示能遵循特定日期格式。为此,我们将使用此前用于应用程序国际化的 <s:text> 标签。

此处使用的消息键为 date.format。该键位于文件 [FormDateModel.properties] 中:


date.format={0,date,dd/MM/yyyy}

该键对应的值并非消息本身,而是一种显示格式:

  • 0 是一个代表待显示值的参数。在此处,它将对应模型中的字段 date5
  • date 表示日期。此前,number 表示数字。
  • dd/MM/yyyy 表示日期格式。此处要求格式为 dd/mm/yyyy。相应的 Java 格式为 dd/MM/yyyy(d:日,M:月,y:年)

标签


<s:text name="date.format">
            <s:param value="date5"/>
</s:text>

将显示消息 {0, date, dd/MM/yyyy},其中参数 date5(第 2 行)将替换格式中的参数 0。 因此,模板 date5 将以 dd/mm/yyyy 的格式显示。

14.6. 模板 [FormDateModel]

表单 [FormDate.jsp] 中的字段 date1date6 被注入到以下模板 [FormDateModel] 中:


package example;

import java.util.Date;

public class FormDateModel {

  // 无参数的构造函数
  public FormDateModel() {
  }
  // 字段
  private String date1;
  private Date date2 = new Date();
  private Date date3 = new Date();
  private Date date4;
  private Date date5;
  private String date6;

  // 清空模型
  public void clearModel() {
    date1 = null;
    date2 = null;
    date3 = null;
    date4 = null;
    date5 = null;
    date6 = null;
  }

  // 获取器和设置器
...
}
  • 字段 date1 date6 的类型为 String
  • 其余字段的类型为 Date

14.7. 模型验证

模型的验证由两个文件控制:[FormDate-validation.xml] 和 [FormDateModel-validation.xml]。

文件 [FormDate-validation.xml] 将验证任务委托给 [FormDateModel-validation.xml]:


<!--
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
-->
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
EN" "http://localhost:8084/example-10/example/xwork-validator-1.0.2.dtd">

<validators>
  <field name="model" >
    <field-validator type="visitor">
      <param name="appendPrefix">false</param>
      <message/>
    </field-validator>
  </field>
</validators>

我们之前已经遇到过这个文件。

文件 [FormDateModel-validation.xml] 包含以下验证规则:


<!--
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
-->

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//
EN" "http://localhost:8084/example-11/example/xwork-validator-1.0.2.dtd">

<validators>

  <field name="date1" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="date1.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^\d{2}/\d{2}/\d{4}$</param>
      <param name="trim">true</param>
      <message key="date1.error"/>
    </field-validator>
  </field>

  <field name="date2" >
    <field-validator type="required" short-circuit="true">
      <message key="date2.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="date2.error"/>
    </field-validator>
   </field>

  <field name="date3" >
    <field-validator type="required" short-circuit="true">
      <message key="date2.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="date3.error"/>
    </field-validator>
    <field-validator type="date" short-circuit="true">
      <param name="min">18/05/2000</param>
      <message key="date3.error"/>
    </field-validator>
  </field>

  <field name="date4" >
    <field-validator type="required" short-circuit="true">
      <message key="date2.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="date4.error"/>
    </field-validator>
    <field-validator type="date" short-circuit="true">
      <param name="max">20/06/2001</param>
      <message key="date4.error"/>
    </field-validator>
  </field>

 <field name="date5" >
    <field-validator type="required" short-circuit="true">
      <message key="date2.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="date5.error"/>
    </field-validator>
    <field-validator type="date" short-circuit="true">
      <param name="min">18/05/2000</param>
      <param name="max">20/06/2001</param>
      <message key="date5.error"/>
    </field-validator>
  </field>

  <field name="date6" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="date6.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^\d{2}/\d{2}/\d{4}$</param>
      <param name="trim">true</param>
      <message key="date6.error"/>
    </field-validator>
  </field>

</validators>
  • 第11至20行的规则用于验证输入字段date1是否符合正则表达式模式,该模式表示格式为dd/mm/yyyy的日期。 需要注意的是,此处仅验证字符串是否符合日期格式,但并不验证其是否为有效日期。因此,29/02/2011 虽然符合日期格式,但并非有效日期。
  • 第 22-29 行的规则用于验证输入字段 date2 是否为有效日期。
  • 第31-42行的规则验证输入字段date3是否为有效日期且日期不早于2000年5月18日
  • 第 44-55 行的规则验证输入字段 date4 是否为有效日期且 <=2001年6月20日。
  • 第 57-69 行的规则验证输入字段 date5 是否为有效日期,且该日期 <=2001年6月20日 且 >=2000年5月18日。
  • 第 71-80 行的规则验证字段 date6 是否采用 dd/mm/yyyy 的日期格式。

当文件 [FormDateModel-validation.xml] 被验证拦截器处理后,如果存在操作 [FormDate],则该拦截器将执行其方法 validate。我们将结合整个操作来介绍该方法。

14.8. 操作 [FormDate]

操作 [FormDate] 如下:


package example;

import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.apache.struts2.interceptor.SessionAware;
import org.apache.struts2.interceptor.validation.SkipValidation;

public class FormDate extends ActionSupport implements ModelDriven, SessionAware {

  // 无参构造函数
  public FormDate() {
  }

  // 操作模型
  public Object getModel() {
    if (session.get("model") == null) {
      session.put("model", new FormDateModel());
    }
    return session.get("model");
  }

  @SkipValidation
  public String clearModel() {
    // 模型清空
    ((FormDateModel) getModel()).clearModel();
    // 结果
    return INPUT;
  }

  public String cancel() {
    // 清理模型
    ((FormDateModel) getModel()).clearModel();
    // 结果
    return "cancel";
  }
  // SessionAware
  Map<String, Object> session;

  public void setSession(Map<String, Object> session) {
    this.session = session;
  }

  // 验证
  @Override
  public void validate() {
    // 日期格式化
    SimpleDateFormat formateurDate = new SimpleDateFormat("dd/MM/yyyy");
    formateurDate.setLenient(false);
    // 日期1输入有效吗?
    if (getFieldErrors().get("date1") == null) {
      // 日期有效性验证
      try {
        formateurDate.parse(((FormDateModel) getModel()).getDate1());
      } catch (Exception e) {
        addFieldError("date1", getText("date1.error"));
      }
    }
    // 日期6输入有效吗?
    if (getFieldErrors().get("date6") == null) {
      Date d = null;
      try {
        // 日期有效性验证
        d = formateurDate.parse(((FormDateModel) getModel()).getDate6());
        // 边界检查
        if (d.after(formateurDate.parse("20/06/2001")) || d.before(formateurDate.parse("18/05/2000"))) {
          addFieldError("date6", getText("date6.error"));
          return;
        }
      } catch (Exception e) {
        addFieldError("date6", getText("date6.error"));
        return;
      }
    }
  }
}

操作 [FormDate] 基于与操作 [FormInt] 相同的模型构建。 本文仅对方法 validate 进行说明。需要说明的是,方法 validate 在处理验证文件 [FormDateModel-validation.xml] 之后、执行方法 execute 之前执行。

  • 方法 validate 完成了对 date1 date6 日期验证的补充。验证文件 [FormDateModel-validation.xml] 已验证所输入的字符串是否符合日期格式。 现在需要验证这些是否确实是有效的日期。同时需要指出,date1 date6 是该表单中仅有的两个 String 类型的字段。
  • 第 50 行:创建日期格式 dd/mm/yyyy,以验证此类字符串是否为有效日期。
  • 第 51 行:setLenient(false) 可防止无效日期 32/01/2012 被解释为有效日期 01/02/2012。
  • 第53行:仅当前面的验证在此字段上未出现错误时,才验证date1
  • 第 56 行:验证 date1 是否为符合 dd/mm/yyyy 格式的有效日期。
  • 第 58 行:若不符合要求,则为字段 date1 关联一条错误信息。
  • 第 62 行:仅当该字段已通过前面的验证时,才对 date6 进行验证。
  • 第 66 行:验证 date6 是否为有效日期
  • 第 68 行:如果 date6 早于 2000/05/18 或晚于 2001/06/20,则 date6 无效。

14.9. 最后细节

如以下示例所示,前面的表单存在漏洞:

  • 在 [1] 中,输入了无效日期
  • 在 [2] 中,该日期被接受。仅考虑了日期格式的首几个字符(日/月/年)。

Date 类型模板相关的字段 date2date5 均存在此问题。不过,或许是我在文档中遗漏了某些内容。 与 String 类型模板相关的字段 date1date6 则不存在此问题。