Skip to content
Advertisement

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

We have a Struts2 application where we are building survey feature using which application users will be able to create surveys by adding different questions.

These questions are comprised of question text and an html control for getting response.

The html controls so far supported are single and multi-select list, text field, textarea, checkbox and radiobox.

So the survey form renders displaying all questions added by the user where each question has its question text displayed followed by the html field control selected for that question.

Basically, it is a dynamic form where form field names are being dynamically generated as all surveys will be different and therefore there are no properties in the Action class backing survey form fields.

We are generating form field name using prefix question_ appended with database id of the question to represent each question response input field uniquely. Here is a snippet from our JSP page to make things clear.

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

On form submit, in the action class we are using HttpServletRequest to get submitted form field values. The way we identify which question the answer belongs to is through the request parameter name which as can be seen in the above JSP snippet starts with prefix ‘question_’ followed by question Id. So we split the parameter name to get the question id and associate value against that question.

The problem we are facing is with repopulating survey form with submitted values when the page is presented back to the user in case of validation error as the parameter names are dynamic and cannot be backed by properties defined in Action class.

I have tried to populate radio button and textarea fields using the below code and several other ways but to no avail

    <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" />

Below is the action mapping for survey submit action

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

Here is the code in Action class handling the submit logic:

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

Advertisement

Answer

We have finally solved the problem. The initial implementation was heading in a completely wrong direction but thanks to the clue from @RomanC, we re-factored the code and removed the direct use of HttpServletRequest to finally have a working solution. Core idea was to use a Bean for capturing response and have a List of those beans in the Action class corresponding to each survey question. On form submit, response is captured into the bean objects behind the scene by framework itself and thus available for further logic processing in submit handler action method.

Advertisement