Struts2 - 如何在验证错误后使用提交的值重新填充具有动态生成的字段名(通过表达式)的表单

Struts2 - How to repopulate form having dynamically generated fieldnames (via expression) with submitted values after validation error

我们有一个 Struts2 应用程序,我们正在构建调查功能,应用程序用户可以使用该应用程序通过添加不同的问题来创建调查。

这些问题由问题文本和用于获取响应的 html 控件组成。

到目前为止支持的 html 控件是 singlemulti-select list, 文本字段文本区域复选框单选框.

因此调查表呈现显示用户添加的所有问题,其中每个问题都显示其问题文本,后跟 html 字段控件 select 为该问题编辑。

基本上,它是一个动态表单,其中表单字段名称是动态生成的,因为所有调查都不同,因此 Action class 支持调查表单字段中没有属性。

我们正在生成表单字段名称,使用前缀 question_ 附加问题的数据库 ID,以唯一地表示每个问题响应输入字段。这是我们 JSP 页面的一个片段,可以清楚地说明这一点。

    <s:form id="surveyForm" action="survey/submitFeedback">

      <s:iterator value="surveyQuestions">
        <p class="form-group <s:if test="%{fieldErrors.get('question_' + surveyQuestionId).size() > 0}">has-error</s:if>" >
          <label class="control-label" >
            <s:property value="questionText"/>
          </label>
          <s:if test="required" ><span style='color:red'>*</span></s:if>
          <br>
          <s:if test="surveyQuestionType.type == @com.reach150.enumeration.SurveyQuestionTypeEnum@OPENENDED_TEXTFIELD" >
            <s:textfield name="question_%{surveyQuestionId}" cssClass="form-control" maxlength="charactersLimit" />
          </s:if>
          <s:elseif test="surveyQuestionType.type == @com.reach150.enumeration.SurveyQuestionTypeEnum@OPENENDED_TEXTAREA" >
            <s:textarea name="question_%{surveyQuestionId}" style="height: 150px; width: 400px;" cssClass="form-control" maxlength="charactersLimit" />
          </s:elseif>
          <s:elseif test="surveyQuestionType.type == @com.reach150.enumeration.SurveyQuestionTypeEnum@SINGLESELECTDROPDOWN || surveyQuestionType.type == @com.reach150.enumeration.SurveyQuestionTypeEnum@MULTISELECTDROPDOWN" >
            <s:select name="question_%{surveyQuestionId}" list="orderedSelectOptions" listKey="optionValue" listValue="optionLabel" emptyOption="true" multiple="true" cssClass="form-control" />
          </s:elseif>
          <s:else>
            <s:radio name="question_%{surveyQuestionId}" list="#{'true':'Yes','false':'No'}" cssClass="radioMarginRight" />
          </s:else>
          <span class="help-block" for="question_${surveyQuestionId}">
            <s:fielderror cssClass="font-bold text-danger">
              <s:param>question_<s:property value="surveyQuestionId" /></s:param>
            </s:fielderror>
          </span>
          <br/>
        </p>      
      </s:iterator>      
      <button type="submit" class="btn btn-primary btn-lg pull-right "><s:if test="survey.requestReferral == true">Next</s:if><s:else>Done</s:else></button>
      
    </s:form>

在提交表单时,在操作 class 中我们使用 HttpServletRequest 来获取提交的表单字段值。我们识别答案属于哪个问题的方法是通过请求参数名称,如上面的 JSP 片段所示,以前缀 'question_' 开头,后跟问题 ID。因此,我们拆分参数名称以获取问题 ID 并针对该问题关联值。

我们面临的问题是,在验证错误的情况下,当页面返回给用户时,用提交的值重新填充调查表,因为参数名称是动态的,不能由 Action [=60] 中定义的属性支持=].

我尝试使用以下代码和其他几种方法填充 radio buttontextarea 字段,但无济于事

    <s:textarea name="question_%{surveyQuestionId}" style="height: 150px; width: 400px;" cssClass="form-control" maxlength="charactersLimit" value="#parameters.%{'question_' + surveyQuestionId}" />

    <s:radio name="question_%{surveyQuestionId}" value="#parameters.%{'question_' + surveyQuestionId}" list="#{'true':'Yes','false':'No'}" cssClass="radioMarginRight" />

以下是调查提交操作的操作映射

<action name="survey/submitFeedback" class="surveyAction" method="submitFeedback">
    <result name="success" type="tiles">survey.submit</result>
    <result name="error" type="tiles">survey.view</result>
    <param name="public">true</param>
</action>

这是 Action class 中处理提交逻辑的代码:

private Integer npsScore = 0;
private Map<String, String[]> surveyResponseQuestionAnswerMap = new HashMap<>();

    public String submitFeedback() {

    try {
        if (requestId == null) {
            addActionError("Request Id missing! Invalid Request!");
            throw new Exception("Invalid Request!");
        }
        
        surveyRequest = surveyService.getSurveyRequestByUUID(requestId);
        if (surveyRequest == null) {
            addActionError("Request Id Invalid! Invalid Request!");
            throw new Exception("Request Id Invalid! Invalid Request!");
        }
        loadQuestionAnswersMap();
        validateSurveyFeedback();
        
        if (hasErrors()) {
            throw new Exception("Error submitting response!");
        } else {
            surveyService.parseAndSaveSurveyResponse(surveyRequest, surveyResponseQuestionAnswerMap);
            setSurveyCustomMessages(surveyService.getSurveyCustomMessagesSettingBySurveyId(survey.getSurveyId()));
        }
        return SUCCESS;
    } catch (Exception e) {
        addActionError("Error submitting response!");
        logger.error(e);
        loadSurvey();
        return ERROR;
    }
}

private void loadQuestionAnswersMap() {
    HttpServletRequest httpRequest = ActionUtil.getRequest();
    Enumeration<String> parameterNames = httpRequest.getParameterNames();
    while (parameterNames.hasMoreElements()) {
        String parameterName = parameterNames.nextElement();
        if (parameterName.startsWith("question_")) {
            String[] values = httpRequest.getParameterValues(parameterName);
            if (values != null) {
                surveyResponseQuestionAnswerMap.put(parameterName, values);
            }
        }
    }
}

private void validateSurveyFeedback() throws Exception {
    
    HttpServletRequest httpRequest = ActionUtil.getRequest();
    Survey survey = surveyRequest.getSurvey();
    if (survey.isUseNetPromotorScore()) {
        String npsScoreStr = httpRequest.getParameter("npsScore");
        if (StringUtils.isBlank(npsScoreStr)) {
            this.addFieldError("npsScore", "Answer is required");
        } else {
            setNpsScore(Integer.valueOf(npsScoreStr));
        }
    }
    List<SurveyQuestion> requiredQuestions = surveyQuestionService.getRequiredSurveyQuestionsForSurvey(surveyRequest.getSurvey());
    for (SurveyQuestion requiredQuestion : requiredQuestions) {
        Integer requiredQuestionId = requiredQuestion.getSurveyQuestionId();
        String requiredQuestionFieldParameterName = "question_" + requiredQuestionId;
        logger.info("Required Question Field Parameter Name: " + requiredQuestionFieldParameterName);
        String[] answers = httpRequest.getParameterValues(requiredQuestionFieldParameterName);
        if (answers == null) {
            this.addFieldError(requiredQuestionFieldParameterName, "Answer is required");
        } else {
            boolean noValue = true;
            for (String answer : answers) {
                if (StringUtils.isNotBlank(answer)) {
                    noValue = false;
                    break;
                }
            }
            if (noValue) {
                this.addFieldError(requiredQuestionFieldParameterName, "Answer is required");
            }
        }
    }
}

我们终于解决了这个问题。最初的实现朝着完全错误的方向前进,但感谢@RomanC 的线索,我们重构了代码并删除了 HttpServletRequest 的直接使用,最终有了一个可行的解决方案。核心思想是使用一个 Bean 来捕获响应,并在对应于每个调查问题的 Action class 中有一个 List 这些 bean。在表单提交时,响应被框架本身捕获到后台的 bean 对象中,因此可用于提交处理程序操作方法中的进一步逻辑处理。