Skip to content

13. 示例 10 – 实数的转换与验证

新应用程序支持实数输入:

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

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

13.1. NetBeans 项目

NetBeans 项目结构如下:

  • 在 [1] 中,应用程序视图
  • [Accueil.jsp]:主页
  • [FormDouble.jsp]:输入表单
  • [ConfirmationDouble.jsp]:确认页面
  • 在 [2] 中,消息文件 [messages.properties] 和 Struts 主配置文件
  • 在 [3] 中:
  • [FormDouble.java]:用于显示和处理表单的操作
  • [FormDouble-validation.xml]:[FormDouble] 动作的验证规则。该文件通过前文所述的方法,将这些验证任务委托给模型。
  • [FormDoubleModel]:[FormDouble] 操作的模型
  • [FormDoubleModel-validation.xml]:模型的验证规则
  • [FormDoubleModel.properties]:模型的消息文件
  • [example.xml]:辅助的 Struts 配置文件

13.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="FormDouble" class="example.FormDouble">
      <result name="input">/example/FormDouble.jsp</result>
      <result name="cancel" type="redirect">/example/Accueil.jsp</result>
      <result name="success">/example/ConfirmationFormDouble.jsp</result>
    </action>
  </package>
</struts>

这与之前讨论的输入整数的情况类似。

13.3. 消息文件

[messages.properties] 文件内容如下:


Accueil.titre=Accueil
Accueil.message=Struts 2 - Conversions et validations
Accueil.FormDouble=Saisie de nombres r\u00e9els
Form.titre=Conversions et validations
FormDouble.message=Struts 2 - Conversion et validation de nombres r\u00e9els
FormDouble.conseil=Tapez les nombres r\u00e9els avec une virgule comme 10,7
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}".

[FormDoubleModel.properties] 文件内容如下:


double.format={0,number}
double1.prompt=1-Nombre r\u00E9el
double1.error=Tapez un nombre r\u00E9el
double2.prompt=2-Nombre r\u00E9el
double2.error=Tapez un nombre r\u00E9el
double3.prompt=3-Nombre r\u00E9el >=2.64
double3.error=Tapez un nombre r\u00E9el >=2.64
double4.prompt=4-Nombre r\u00E9el <8.32
double4.error=Tapez un nombre r\u00E9el <8.32
double5.prompt=5-Nombre r\u00E9el dans l''intervalle [2.64,8.32[
double5.error=Tapez un nombre r\u00E9el dans l''intervalle [2.64,8.32[
double6.prompt=6-Nombre r\u00E9el dans l''intervalle [2.64,8.32]
double6.error=Tapez un nombre r\u00E9el dans l''intervalle [2.64,8.32]

第 1 行起着重要作用。我们稍后会再回到这一行。

13.4. 输入表单

[FormDouble.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="FormDouble.message"/></h2>
    <h4><s:text name="FormDouble.conseil"/></h4>
    <s:form name="formulaire" action="FormDouble">
      <s:textfield name="double1" key="double1.prompt"/>
      <s:textfield name="double2" key="double2.prompt" value="%{#parameters['double2']!=null ? #parameters['double2'] : double2==null ? '' :getText('double.format',{double2})}"/>
      <s:textfield name="double3" key="double3.prompt" value="%{#parameters['double3']!=null ? #parameters['double3'] : double3==null ? '' :getText('double.format',{double3})}"/>
      <s:textfield name="double4" key="double4.prompt" value="%{#parameters['double4']!=null ? #parameters['double4'] : double4==null ? '' :getText('double.format',{double4})}"/>
      <s:textfield name="double5" key="double5.prompt" value="%{#parameters['double5']!=null ? #parameters['double5'] : double5==null ? '' :getText('double.format',{double5})}"/>
      <s:textfield name="double6" key="double6.prompt"/>
      <s:submit key="Form.submitText" method="execute"/>
    </s:form>
    <br/>
    <s:url id="url" action="FormDouble" method="cancel"/>
    <s:a href="%{url}"><s:text name="Form.cancelText"/></s:a>
    <br/>
    <s:url id="url" action="FormDouble" method="clearModel"/>
    <s:a href="%{url}"><s:text name="Form.clearModel"/></s:a>
  </body>
</html>

第 13 行至第 18 行是六个实数输入字段。字段 double2double5 具有 complex 属性。通常,这六个输入字段应如下所示:


      <s:textfield name="double1" key="double1.prompt"/>
      <s:textfield name="double2" key="double2.prompt"/>
      <s:textfield name="double3" key="double3.prompt"/>
      <s:textfield name="double4" key="double4.prompt"/>
      <s:textfield name="double5" key="double5.prompt"/>
<s:textfield name="double6" key="double6.prompt"/>

为了解决测试过程中遇到的某些问题,我们不得不增加一些复杂性。目前,读者可以忽略这些复杂细节。我们稍后会进行说明。

13.5. 确认视图

确认视图 [ConfirmationFormDouble.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="double1.prompt"/></td>
        <td><s:text name="double1"/></td>
      </tr>
      <tr>
        <td><s:text name="double2.prompt"/></td>
        <td>
          <s:text name="double.format">
            <s:param value="double2"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="double3.prompt"/></td>
        <td>
          <s:text name="double.format">
            <s:param value="double3"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="double4.prompt"/></td>
        <td>
          <s:text name="double.format">
            <s:param value="double4"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="double5.prompt"/></td>
        <td>
          <s:text name="double.format">
            <s:param value="double5"/>
          </s:text>
        </td>
      </tr>
      <tr>
        <td><s:text name="double6.prompt"/></td>
        <td><s:text name="double6"/></td>
      </tr>
    </table>
    <br/>
    <s:url id="url" action="FormDouble!input"/>
    <s:a href="%{url}"><s:text name="Confirmation.lien"/></s:a>
  </body>
</html>

[ConfirmationFormDouble.jsp] 页面仅用于显示 [FormDoubleModel]。第 47–49 行展示了实数的显示方式。 我们希望此显示能考虑应用程序的区域设置。根据区域设置的不同,实数在法国(10,7)和英国(10.7)的显示方式将有所不同。为实现这一目标,我们使用 <s:text> 标签,该标签此前已用于应用程序的国际化。因此,此标签也用于本地化。

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


double.format={0,number}

此处与该键关联的值并非消息,而是一种显示格式:

  • 0 是一个参数,表示待显示的值。在此处,它将对应模型中的 double5 字段。
  • number 表示数字格式。该格式将根据不同国家进行适配。若未指定此格式,数字 10.7 将始终显示为 10.7,无论所在国家如何。

该标签


         <s:text name="double.format">
            <s:param value="double5"/>
</s:text>

显示消息 {0, number},其中 double5 参数(第 2 行)替换了格式中的 0 参数。因此,double5 的值将以本地化的数字格式显示。

13.6. [FormDoubleModel] 模板

来自 [FormDouble.jsp] 表单的 double1double6 字段被注入到以下 [FormDoubleModel] 模板中:


package example;
 
public class FormDoubleModel {
 
  // constructor without parameters
  public FormDoubleModel() {
  }
  // fields
  private String double1;
  private Double double2;
  private Double double3;
  private Double double4;
  private Double double5;
  private String double6;
 
  // raz model
  public void clearModel() {
    double1 = null;
    double2 = null;
    double3 = null;
    double4 = null;
    double5 = null;
    double6 = null;
  }
 
  // getters and setters
   ...
}
  • 字段 double1 double6 的类型为 String
  • 其余字段的类型为 Double

13.7. 模型验证

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

[FormDouble-validation.xml] 文件将验证任务委托给 [FormDoubleModel-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/exemple-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>

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

[FormDoubleModel-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/exemple-10/example/xwork-validator-1.0.2.dtd">
 
 
<validators>
 
  <field name="double1" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="double1.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^[+|-]*\s*\d+(,\d+)*$</param>
      <param name="trim">true</param>
      <message key="double1.error"/>
    </field-validator>
  </field>
 
  <field name="double2" >
    <field-validator type="required" short-circuit="true">
      <message key="double2.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="double2.error"/>
    </field-validator>
  </field>
 
  <field name="double3" >
    <field-validator type="required" short-circuit="true">
      <message key="double3.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="double3.error"/>
    </field-validator>
    <field-validator type="double" short-circuit="true">
      <param name="minInclusive">2.64</param>
      <message key="double3.error"/>
    </field-validator>
  </field>
 
  <field name="double4" >
    <field-validator type="required" short-circuit="true">
      <message key="double4.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="double4.error"/>
    </field-validator>
    <field-validator type="double" short-circuit="true">
      <param name="maxExclusive">8.32</param>
      <message key="double4.error"/>
    </field-validator>
  </field>
 
  <field name="double5" >
    <field-validator type="required" short-circuit="true">
      <message key="double5.error"/>
    </field-validator>
    <field-validator type="conversion" short-circuit="true">
      <message key="double5.error"/>
    </field-validator>
    <field-validator type="double" short-circuit="true">
      <param name="minInclusive">2.64</param>
      <param name="maxExclusive">8.32</param>
      <message key="double5.error"/>
    </field-validator>
  </field>
 
  <field name="double6" >
    <field-validator type="requiredstring" short-circuit="true">
      <message key="double6.error"/>
    </field-validator>
    <field-validator type="regex" short-circuit="true">
      <param name="expression">^[+|-]*\s*\d+(,\d+)*$</param>
      <param name="trim">true</param>
      <message key="double6.error"/>
    </field-validator>
  </field>
</validators>
  • 第 12–21 行中的规则用于验证 double1 输入字段是否符合表示实数的正则表达式模式。
  • 第 23–30 行的规则验证输入字段 `double2` 能否转换为双精度实数。
  • 第 32–43 行的规则验证输入字段 `double3` 能否转换为大于或等于 2.64 的双精度实数。请注意,必须使用英式实数表示法。
  • 第 45–56 行的规则验证 double4 输入字段能否转换为小于 8.32 的双精度实数。
  • 第 58–70 行中的规则验证输入字段 double5 能否转换为位于区间 [2.64, 8.32[ 内的双精度实数。
  • 第 72–80 行中的规则验证了 double6 字段符合表示实数的正则表达式模式。

一旦 [FormDoubleModel-validation.xml] 文件被验证拦截器处理完毕,后者就会执行 [FormDouble] 动作的 validate 方法(如果该方法存在)。我们将结合整个动作来介绍它。

13.8. [FormDouble] 操作

[FormDouble] 操作如下:


package example;
 
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import java.util.Map;
import org.apache.struts2.interceptor.SessionAware;
import org.apache.struts2.interceptor.validation.SkipValidation;
 
public class FormDouble extends ActionSupport implements ModelDriven, SessionAware {
 
  // constructor without parameters
  public FormDouble() {
  }
 
  // action model
  public Object getModel() {
    if (session.get("model") == null) {
      session.put("model", new FormDoubleModel());
    }
    return session.get("model");
  }

  @SkipValidation
  public String clearModel() {
    // close to the model
    ((FormDoubleModel) getModel()).clearModel();
    // result
    return INPUT;
  }
 
  public String cancel() {
    // cleaning the model
    ((FormDoubleModel) getModel()).clearModel();
    // result
    return "cancel";
  }
  // SessionAware
  Map<String, Object> session;
 
  public void setSession(Map<String, Object> session) {
    this.session = session;
  }
 
  // validation
  @Override
  public void validate() {
    // valid double6 entry?
    if (getFieldErrors().get("double6") == null) {
      // replace the comma with a period in the double6 string
      String strDouble6 = (((FormDoubleModel) getModel()).getDouble6()).replace(',', '.');
      // String --> double
      double double6 = Double.parseDouble(strDouble6);
      // check
      if (double6 < 2.64 || double6 > 8.32) {
        addFieldError("double6", getText("double6.error"));
      }
    }
  }
}

[FormDouble] 操作基于与 [FormInt] 操作相同的模型构建。我们仅对 validate 方法进行说明。请注意,validate 方法在处理完验证文件 [FormDoubleModel-validation.xml] 之后、调用 execute 方法之前执行。

  • 第 48 行:如果 double6 字段中已经存在错误,则不再采取任何进一步操作。
  • 第 50 行:我们接收到了一个格式为 45.67 的字符串。该字符串被存储在模型的 double6 字段中。我们将逗号替换为小数点,得到 45.67。
  • 第 52 行:将字符串 45.67 转换为双精度浮点数。由于 double6 字段中的字符串符合实数格式,此操作应能正常进行。
  • 第 54 行:我们检查生成的 double 值是否位于区间 [2.64, 8.32] 内。
  • 第 55 行:若不满足此条件,则将 double6.error 键对应的错误消息附加到 double6 字段。该消息可在 [FormDoubleModel.properties] 文件中找到,并在包含该错误的表单重新显示时呈现。

13.9. 最终细节

现在,让我们回到 [FormDouble.jsp] 表单中输入字段的复杂性问题。我们将重点关注 double2 字段。这一逻辑同样适用于采用 Double 模型的 double3double5 字段。对于采用 String 模型的 double1double6 字段,则不存在此类问题。

double2 输入字段如下:


<s:textfield name="double2" key="double2.prompt" value="%{#parameters['double2']!=null ? #parameters['double2'] : double2==null ? '' :getText('double.format',{double2})}"/>

让我们从最简单的标签开始:


<s:textfield name="double2" key="double2.prompt"/>

来看看会发生什么:

  • 在 [1] 中,我们验证了 double2 字段的数值是否正确。虽然截图中不太明显,但我们输入的数值带有小数点。
  • 在 [2] 中,是确认视图。double2 字段已通过验证检查。该数值显示时带有小数点。
  • 在[3]中,我们返回表单
  • 在[4]中,表单。截图中未清晰显示的是,double2字段的数值(初始值为4.32)已变为带小数点的4.32。
  • 在 [5] 中,我们未作任何修改便重新提交了表单
  • 在[6]中,double2字段报告了错误。

问题如下:

  • 最初在[1]中,输入字符串“4,32”已成功转换为浮点数4.32。这意味着字符串到双精度浮点数的转换成功,且在此情境下,Struts考虑了区域设置——本例中为法国。
  • 新表单[4]在 double2 字段中显示了实数 4.32。由于显示未进行本地化,因此默认采用盎格鲁-撒克逊格式,即使用小数点。因此,在 Double 转 String 的方向上,Struts 不再考虑区域设置;否则,它本应显示带逗号的 4.32。

这至少可以说是不一致的。不过没关系,我们将对数字 4.32 的显示进行本地化。输入标签变为如下所示:


<s:textfield name="double2" key="double2.prompt" value="%{getText('double.format',{double2})}"/>

value 属性指定要在 double2 字段中显示的值。这是一个 OGNL 表达式。回顾 [FormDoubleModel.properties] 文件中 double.format 键的定义:


double.format={0,number}

getText('key') 方法用于检索与某个键关联的消息。该消息会在与当前区域设置对应的文件中进行查找。因此,如果区域设置为 es(西班牙),则会在 [FormDoubleModel_es.properties] 文件中查找 double.format 键。

getText('key', {param0, param1, ...}) 方法用于检索带参数的消息。该消息

{0, number}

是由参数 0 参数化的消息。这是一个位置参数。getText('double.format', {double2}) 方法将把数字 double2 赋值给参数 0。最终,我们请求以本地化数字格式显示 double2 的值。在法国,数字 4.56 将被本地化为字符串 "4,56"。

完成此更改后,我们再次运行测试。

表单初次显示时,[1] 处便出现了异常。让我们回到该标签:


<s:textfield name="double2" key="double2.prompt" value="%{getText('double.format',{double2})}"/>

在初始显示时,double2 模型的值为 null,即非数值。我们将标签修改如下:


<s:textfield name="double2" key="double2.prompt" value="%{double2==null ? '' : getText('double.format',{double2})}"/>

这次,我们检查 double2 是否为 null。如果是,则显示一个空字符串。

完成此修改后,我们继续进行测试:

屏幕截图 [1] 至 [4] 显示,我们试图解决的问题已得到解决:

  • 在[1]中,我们输入4.67
  • 在[2]中,该数值被成功接受
  • 在[3]中,该数值再次显示为带小数点的4.67,这一点在[4]中得到了确认。

遗憾的是,问题并未就此结束。让我们来看一下以下序列:

  • 在[5]中,添加了一个字符导致double2失效
  • 在[6]中,验证检查已发挥作用并报告了错误。然而,[6]中显示的字符串并非错误字符串,而是double2模型的当前值。输入的值已丢失。

从 [5] 跳转至 [6] 时,请求并未完成。它被验证拦截器拦截了。在 [6] 中,我们显示了 double2 模型的值,由于此次中断,该模型并未接收新值。 因此,显示的是其先前值,而本应显示的是输入的字符串。可通过 #parameters['param'] 语法访问请求的参数。我们按如下方式更新 double2 输入字段:


      <s:textfield name="double2" key="double2.prompt" value="%{#parameters['double2']!=null ? #parameters['double2'] : double2==null ? '' : getText('double.format',{double2})}"/>

double2 字段的显示值计算方式如下:如果存在 'double2' 参数,则显示该参数;否则,显示 double2 模板。欢迎读者测试此新版标签是否解决了遇到的问题。

13.10. 结论

令人惊讶的是,带有效性检查的实数输入竟然如此复杂……或许是我在文档中遗漏了什么?