Repository: struts Updated Branches: refs/heads/master d923c7c4f -> 3163d6c38
Adds new result 'JSONActionRedirectResult' to json-plugin. Contains tests and example in showcase app. The new result type is intended to be used along with existing JSONValidationInterceptor. It makes it possible to do form validation via ajax and handle form submitting, action execution and redirect evaluation all in one request inside JS context. Project: http://git-wip-us.apache.org/repos/asf/struts/repo Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/cb118984 Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/cb118984 Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/cb118984 Branch: refs/heads/master Commit: cb11898416949c4028d99651e9697671797f1b99 Parents: a4f8d49 Author: cnenning <cnenn...@apache.org> Authored: Mon Jan 11 14:36:32 2016 +0100 Committer: cnenning <cnenn...@apache.org> Committed: Mon Jan 11 14:36:32 2016 +0100 ---------------------------------------------------------------------- .../validation/AjaxFormSubmitAction.java | 152 ++++++++++++++++ .../validation/AjaxFormSubmitSuccessAction.java | 7 + .../src/main/resources/struts-validation.xml | 13 ++ .../ajaxErrorContainers/actionerror.ftl | 46 +++++ .../ajaxErrorContainers/controlfooter.ftl | 39 ++++ .../ajaxErrorContainers/controlheader-core.ftl | 80 +++++++++ .../ajaxErrorContainers/theme.properties | 21 +++ .../src/main/webapp/WEB-INF/decorators/main.jsp | 2 + .../WEB-INF/validation/ajaxFormSubmit.jsp | 177 +++++++++++++++++++ .../validation/ajaxFormSubmitSuccess.jsp | 17 ++ .../struts2/json/JSONActionRedirectResult.java | 71 ++++++++ .../struts2/json/JSONValidationInterceptor.java | 6 +- .../json/src/main/resources/struts-plugin.xml | 1 + .../json/JSONActionRedirectResultTest.java | 105 +++++++++++ 14 files changed, 735 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitAction.java ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitAction.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitAction.java new file mode 100644 index 0000000..b179370 --- /dev/null +++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitAction.java @@ -0,0 +1,152 @@ +package org.apache.struts2.showcase.validation; + +import java.sql.Date; + +import com.opensymphony.xwork2.validator.annotations.DateRangeFieldValidator; +import com.opensymphony.xwork2.validator.annotations.EmailValidator; +import com.opensymphony.xwork2.validator.annotations.FieldExpressionValidator; +import com.opensymphony.xwork2.validator.annotations.IntRangeFieldValidator; +import com.opensymphony.xwork2.validator.annotations.RegexFieldValidator; +import com.opensymphony.xwork2.validator.annotations.RequiredFieldValidator; +import com.opensymphony.xwork2.validator.annotations.RequiredStringValidator; +import com.opensymphony.xwork2.validator.annotations.StringLengthFieldValidator; +import com.opensymphony.xwork2.validator.annotations.UrlValidator; + +/** + * <!-- START SNIPPET: ajaxFormSubmit --> + */ +/** + * Example Action that shows how forms can be validated and submitted via AJAX + * only. Form-submit-and-page-reload functionality of browsers is not used for + * this action. + * <p>Some things to note: + * <ul> + * <li>Depends on <code>json-plugin</code>.</li> + * <li>Requires <code>jsonValidationInterceptor</code> to be on stack.</li> + * <li>Uses a special json redirect result type.</li> + * <li>Uses http parameters <code>struts.enableJSONValidation=true</code> and <code>struts.validateOnly=false</code>.</li> + * <li>Uses a customized theme to make sure html elements required as error containers are always present and easily selectable in JS.</li> + * <li>Uses some custom JS code depending on jQuery to issue AJAX request and to render errors in html.</li> + * <li>Shows visual feedback while waiting for AJAX response.</li> + * </ul> + * </p> + * + */ +public class AjaxFormSubmitAction extends AbstractValidationActionSupport { + + private String requiredValidatorField = null; + private String requiredStringValidatorField = null; + private Integer integerValidatorField = null; + private Date dateValidatorField = null; + private String emailValidatorField = null; + private String urlValidatorField = null; + private String stringLengthValidatorField = null; + private String regexValidatorField = null; + private String fieldExpressionValidatorField = null; + + @Override + public void validate() { + if (hasFieldErrors()) { + addActionError("Errors present!"); + } + } + + public Date getDateValidatorField() { + return dateValidatorField; + } + + @DateRangeFieldValidator( + min="01/01/1990", + max="01/01/2000", + message="must be a min 01-01-1990 max 01-01-2000 if supplied") + public void setDateValidatorField(Date dateValidatorField) { + this.dateValidatorField = dateValidatorField; + } + + public String getEmailValidatorField() { + return emailValidatorField; + } + + @EmailValidator(message="must be a valid email if supplied") + public void setEmailValidatorField(String emailValidatorField) { + this.emailValidatorField = emailValidatorField; + } + + public Integer getIntegerValidatorField() { + return integerValidatorField; + } + + @IntRangeFieldValidator(min="1", max="10", message="must be integer min 1 max 10 if supplied") + public void setIntegerValidatorField(Integer integerValidatorField) { + this.integerValidatorField = integerValidatorField; + } + + public String getRegexValidatorField() { + return regexValidatorField; + } + + @RegexFieldValidator( + regex="[^<>]+", + message="regexValidatorField must match a regexp (.*\\.txt) if specified") + public void setRegexValidatorField(String regexValidatorField) { + this.regexValidatorField = regexValidatorField; + } + + public String getRequiredStringValidatorField() { + return requiredStringValidatorField; + } + + @RequiredStringValidator(trim=true, message="required and must be string") + public void setRequiredStringValidatorField(String requiredStringValidatorField) { + this.requiredStringValidatorField = requiredStringValidatorField; + } + + public String getRequiredValidatorField() { + return requiredValidatorField; + } + + @RequiredFieldValidator(message="required") + public void setRequiredValidatorField(String requiredValidatorField) { + this.requiredValidatorField = requiredValidatorField; + } + + public String getStringLengthValidatorField() { + return stringLengthValidatorField; + } + + @StringLengthFieldValidator( + minLength="2", + maxLength="4", + trim=true, + message="must be a String of a specific greater than 1 less than 5 if specified") + public void setStringLengthValidatorField(String stringLengthValidatorField) { + this.stringLengthValidatorField = stringLengthValidatorField; + } + + public String getFieldExpressionValidatorField() { + return fieldExpressionValidatorField; + } + + @FieldExpressionValidator( + expression = "(fieldExpressionValidatorField == requiredValidatorField)", + message = "must be the same as the Required Validator Field if specified") + public void setFieldExpressionValidatorField( + String fieldExpressionValidatorField) { + this.fieldExpressionValidatorField = fieldExpressionValidatorField; + } + + public String getUrlValidatorField() { + return urlValidatorField; + } + + @UrlValidator(message="must be a valid url if supplied") + public void setUrlValidatorField(String urlValidatorField) { + this.urlValidatorField = urlValidatorField; + } +} + +/** + * <!-- END SNIPPET: ajaxFormSubmit --> + */ + + http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitSuccessAction.java ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitSuccessAction.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitSuccessAction.java new file mode 100644 index 0000000..6eca3fc --- /dev/null +++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/validation/AjaxFormSubmitSuccessAction.java @@ -0,0 +1,7 @@ +package org.apache.struts2.showcase.validation; + +public class AjaxFormSubmitSuccessAction { + public String execute() { + return "success"; + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/resources/struts-validation.xml ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/resources/struts-validation.xml b/apps/showcase/src/main/resources/struts-validation.xml index bab906e..40b9899 100755 --- a/apps/showcase/src/main/resources/struts-validation.xml +++ b/apps/showcase/src/main/resources/struts-validation.xml @@ -37,6 +37,19 @@ <result name="input">quiz-ajax.jsp</result> <result>quiz-success.jsp</result> </action> + + <!-- =========================================== --> + <!-- === ajax form submit === --> + <!-- =========================================== --> + <action name="ajaxFormSubmit" class="org.apache.struts2.showcase.validation.AjaxFormSubmitAction"> + <interceptor-ref name="jsonValidationWorkflowStack" /> + <result name="input">/WEB-INF/validation/ajaxFormSubmit.jsp</result> + <result type="jsonActionRedirect">ajaxFormSubmitSuccess</result> + </action> + <action name="ajaxFormSubmitSuccess" class="org.apache.struts2.showcase.validation.AjaxFormSubmitSuccessAction"> + <result>/WEB-INF/validation/ajaxFormSubmitSuccess.jsp</result> + </action> + </package> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/resources/template/ajaxErrorContainers/actionerror.ftl ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/resources/template/ajaxErrorContainers/actionerror.ftl b/apps/showcase/src/main/resources/template/ajaxErrorContainers/actionerror.ftl new file mode 100644 index 0000000..3f7d8ab --- /dev/null +++ b/apps/showcase/src/main/resources/template/ajaxErrorContainers/actionerror.ftl @@ -0,0 +1,46 @@ +<#-- +/* + * $Id$ + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +--> +<#-- + Make sure element is always present. To be filled later via JS. +--> +<ul<#rt/> +<#if parameters.id??> + id="${parameters.id?html}"<#rt/> +</#if> +<#if parameters.cssClass??> + class="${parameters.cssClass?html}"<#rt/> +<#else> + class="errorMessage"<#rt/> +</#if> +<#if parameters.cssStyle??> + style="${parameters.cssStyle?html}"<#rt/> +</#if> +> +<#if (actionErrors?? && actionErrors?size > 0)> + <#list actionErrors as error> + <#if error??> + <li><span><#if parameters.escape>${error!?html}<#else>${error!}</#if></span><#rt/></li><#rt/> + </#if> + </#list> +</#if> +</ul> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlfooter.ftl ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlfooter.ftl b/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlfooter.ftl new file mode 100644 index 0000000..7f95111 --- /dev/null +++ b/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlfooter.ftl @@ -0,0 +1,39 @@ +<#-- +/* + * $Id$ + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +--> +${parameters.after!}<#t/> + </td><#lt/> +</tr> +<#if (parameters.errorposition!"top") == 'bottom'> +<#assign hasFieldErrors = parameters.name?? && fieldErrors?? && fieldErrors[parameters.name]??/> +<#if hasFieldErrors> +<tr errorFor="${parameters.id}"> + <td class="tdErrorMessage" colspan="2"><#rt/> + <#if hasFieldErrors> + <#list fieldErrors[parameters.name] as error> + <div class="errorMessage">${error?html}</div><#t/> + </#list> + </#if> + </td><#lt/> +</tr> +</#if> +</#if> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlheader-core.ftl ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlheader-core.ftl b/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlheader-core.ftl new file mode 100644 index 0000000..c07b867 --- /dev/null +++ b/apps/showcase/src/main/resources/template/ajaxErrorContainers/controlheader-core.ftl @@ -0,0 +1,80 @@ +<#-- +/* + * $Id$ + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +--> +<#-- + Always include elements to show errors. They may be filled later via AJAX. +--> +<#assign hasFieldErrors = parameters.name?? && fieldErrors?? && fieldErrors[parameters.name]??/> +<#if (parameters.errorposition!"top") == 'top'> +<tr errorFor="${parameters.id}"> + <td class="tdErrorMessage" colspan="2" data-error-for-fieldname="${parameters.name}"><#rt/> + <#if hasFieldErrors> + <#list fieldErrors[parameters.name] as error> + <div class="errorMessage">${error?html}</div><#t/> + </#list> + </#if> + </td><#lt/> +</tr> +</#if> +<#if !parameters.labelposition?? && (parameters.form.labelposition)??> +<#assign labelpos = parameters.form.labelposition/> +<#elseif parameters.labelposition??> +<#assign labelpos = parameters.labelposition/> +</#if> +<#-- + if the label position is top, + then give the label it's own row in the table +--> +<tr> +<#if (labelpos!"") == 'top'> + <td class="tdLabelTop" colspan="2"><#rt/> +<#else> + <td class="tdLabel"><#rt/> +</#if> +<#if parameters.label??> + <label <#t/> +<#if parameters.id??> + for="${parameters.id?html}" <#t/> +</#if> +<#if hasFieldErrors> + class="errorLabel"<#t/> +<#else> + class="label"<#t/> +</#if> + ><#t/> +<#if parameters.required!false && parameters.requiredPosition!"right" != 'right'> + <span class="required">*</span><#t/> +</#if> +${parameters.label?html}<#t/> +<#if parameters.required!false && parameters.requiredPosition!"right" == 'right'> + <span class="required">*</span><#t/> +</#if> +${parameters.labelseparator!":"?html}<#t/> +<#include "/${parameters.templateDir}/${parameters.expandTheme}/tooltip.ftl" /> +</label><#t/> +</#if> + </td><#lt/> +<#-- add the extra row --> +<#if (labelpos!"") == 'top'> +</tr> +<tr> +</#if> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/resources/template/ajaxErrorContainers/theme.properties ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/resources/template/ajaxErrorContainers/theme.properties b/apps/showcase/src/main/resources/template/ajaxErrorContainers/theme.properties new file mode 100644 index 0000000..81346c7 --- /dev/null +++ b/apps/showcase/src/main/resources/template/ajaxErrorContainers/theme.properties @@ -0,0 +1,21 @@ +# +# $Id$ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +parent = xhtml http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp index 22ffc60..4c25c58 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp @@ -188,6 +188,7 @@ <s:url var="clientSideValidationUrl" action="clientSideValidationExample" namespace="/validation"/> <s:url var="storeMessageAcrossRequestExample" namespace="/validation" action="storeErrorsAcrossRequestExample"/> <s:url var="beanValidationUrl" action="bean-validation" namespace="/bean-validation"/> + <s:url var="ajaxFormSubmitUrl" action="ajaxFormSubmit" namespace="/validation" method="input"/> <li><s:a href="%{beanValidationUrl}">Bean Validation</s:a></li> <li><s:a href="%{fieldValidatorUrl}">Field Validators</s:a></li> <li><s:a href="%{clientSideValidationUrl}">Field Validators with client-side JavaScript</s:a></li> @@ -197,6 +198,7 @@ <li><s:a href="%{quizClient}">Validation (client)</s:a></li> <li><s:a href="%{quizClientCss}">Validation (client using css_xhtml theme)</s:a></li> <li><s:a href="%{visitorValidatorUrl}">Visitor Validator</s:a></li> + <li><s:a href="%{ajaxFormSubmitUrl}">AJAX Form Submit</s:a></li> </ul> </li> <li class="dropdown"> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmit.jsp ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmit.jsp b/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmit.jsp new file mode 100644 index 0000000..c327802 --- /dev/null +++ b/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmit.jsp @@ -0,0 +1,177 @@ +<%@taglib prefix="s" uri="/struts-tags" %> + +<html> +<head> + <title>Struts2 Showcase - Validation - AJAX Form Submit</title> + <s:head theme="xhtml"/> + + <style type="text/css"> + /* see comment of script element below! */ + .ajaxVisualFeedback { + width: 16px; + height: 16px; + background-image: url('../images/indicator.gif'); + background-repeat: no-repeat; + float: right; + } + </style> + +</head> +<body> + +<div class="page-header"> + <h1>AJAX Form Submit</h1> +</div> + +<div class="container-fluid"> + <div class="row"> + <div class="col-md-12"> + + <!-- START SNIPPET: ajaxFormSubmit --> + + <h3>Action Errors Will Appear Here</h3> + <s:actionerror theme="ajaxErrorContainers"/> + + <hr/> + + <s:form method="POST" theme="xhtml"> + <s:textfield label="Required Validator Field" name="requiredValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="Required String Validator Field" name="requiredStringValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="Integer Validator Field" name="integerValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="Date Validator Field" name="dateValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="Email Validator Field" name="emailValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="URL Validator Field" name="urlValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="String Length Validator Field" name="stringLengthValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="Regex Validator Field" name="regexValidatorField" theme="ajaxErrorContainers"/> + <s:textfield label="Field Expression Validator Field" name="fieldExpressionValidatorField" theme="ajaxErrorContainers"/> + <s:submit label="Submit" cssClass="btn btn-primary"/> + </s:form> + + <!-- END SNIPPET: ajaxFormSubmit --> + </div> + </div> +</div> + +<script type="text/javascript"> +/******************************************************************** + * JS just used on this page. + * Usually this would be placed in a JS file + * but as this showcase app is already hard to follow + * I place it here so it is easier to find. + * + * note: this requires jQuery. + *******************************************************************/ + + /** + * Validates given form per AJAX. To be called as onSubmit handler. + * + * @param event onSubmit event + */ +function ajaxFormValidation(event) { + event.preventDefault(); + _removeValidationErrors(); + + var _form = $(event.target); + var _formData = _form.serialize(true); + + // prepare visual feedback + // you may want to use other elements here + var originalButton = _form.find('.btn-primary'); + // note: jQuery returns an array-like object + if (originalButton && originalButton.length && originalButton.length > 0) { + originalButton.hide(); + var feedbackElement = $('<div class="ajaxVisualFeedback"></div>').insertAfter(originalButton); + var restoreFunction = function() { + originalButton.show(); + feedbackElement.remove(); + } + } + + + var options = { + data: 'struts.enableJSONValidation=true&struts.validateOnly=false&' + _formData, + async: true, + processData: false, + type: 'POST', + success: function (response, statusText, xhr) { + if (response.location) { + // no validation errors + // action has been executed and sent a redirect URL wrapped as JSON + // cannot use a normal http-redirect (status-code 3xx) as this would be followed by browsers and would not be available here + + // follow JSON-redirect + window.location.href = response.location; + } else { + if (restoreFunction) { + restoreFunction(); + } + _handleValidationResult(_form, response); + } + }, + error: function(xhr, textStatus, errorThrown) { + if (restoreFunction) { + restoreFunction(); + } + // show user an error message + _handleValidationResult(_form, {errors: ['Network or server error!']}) + } + } + + // send request, after delay to make sure everybody notices the visual feedback :) + window.setTimeout(function() { + var url = _form[0].action; + jQuery.ajax(url, options); + }, 1000); +} + +/** + * Removes validation errors from HTML DOM. + */ +function _removeValidationErrors() { + // action errors + // you might want to use a custom ID here + $('ul.errorMessage li').remove(); + + // field errors + $('div.errorMessage').remove(); +} + +/** + * Incorporates validation errors in HTML DOM. + * + * @param form Form containing errors. + * @param errors Errors from server. + * @returns {Boolean} True if form can be submitted. + */ +function _handleValidationResult(form, errors) { + // action errors + if (errors.errors) { + // you might want to use a custom ID here + var errorContainer = $('ul.errorMessage'); + $.each(errors.errors, function(index, errorMsg) { + var li = $('<li><span></span></li>'); + li.text(errorMsg); + errorContainer.append(li); + }); + } + + // field errors + if (errors.fieldErrors) { + $.each(errors.fieldErrors, function(fieldName, errorMsg) { + var td = $('td[data-error-for-fieldname="' + fieldName + '"]'); + if (td) { + var div = $('<div class="errorMessage"></div>'); + div.text(errorMsg); + td.append(div); + } + }); + } +} + +// register onSubmit handler +$(window).bind('load', function() { + $('form').bind('submit', ajaxFormValidation); +}); +</script> +</body> +</html> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmitSuccess.jsp ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmitSuccess.jsp b/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmitSuccess.jsp new file mode 100644 index 0000000..f8a0731 --- /dev/null +++ b/apps/showcase/src/main/webapp/WEB-INF/validation/ajaxFormSubmitSuccess.jsp @@ -0,0 +1,17 @@ +<%@taglib prefix="s" uri="/struts-tags" %> +<html> +<head> + <title>Struts2 Showcase - Validation - Success Field Validators Example</title> + <s:head/> +</head> +<body> + +<div class="page-header"> + <h1>Success !</h1> +</div> + +<div class="container-fluid"> +Form has been submitted. +</div> +</body> +</html> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/plugins/json/src/main/java/org/apache/struts2/json/JSONActionRedirectResult.java ---------------------------------------------------------------------- diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONActionRedirectResult.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONActionRedirectResult.java new file mode 100644 index 0000000..f0f3f39 --- /dev/null +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONActionRedirectResult.java @@ -0,0 +1,71 @@ +package org.apache.struts2.json; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.result.ServletActionRedirectResult; + +/** + * Specialized form of {@link ServletActionRedirectResult} which takes care of + * situation that browser has a JS/AJAX context, there are no validation errors + * and action is executed. In this case a http redirect is harmful as browsers + * don't pass them to JS handlers. So this result produces a JSON response + * containing redirect data. + * + *<p> + * To be used along with {@link JSONValidationInterceptor}. + *</p> + *<p> + * Response JSON looks like this: + * <pre>{"location": "$redirect url$"}</pre> + *</p> + * + */ +public class JSONActionRedirectResult extends ServletActionRedirectResult { + + private static final long serialVersionUID = 3107276294073879542L; + + @Override + protected void sendRedirect(HttpServletResponse response, String finalLocation) throws IOException { + if (sendJsonInsteadOfRedirect()) { + printJson(response, finalLocation); + } else { + super.sendRedirect(response, finalLocation); + } + } + + /** + * If browser has called action in a JS/AJAX context we cannot send a + * redirect as response. + * + * @return true if a JSON response shall be generated, false if a redirect + * shall be sent. + */ + static boolean sendJsonInsteadOfRedirect() { + HttpServletRequest request = ServletActionContext.getRequest(); + return isJsonEnabled(request) && !isValidateOnly(request); + } + + static void printJson(HttpServletResponse response, String finalLocation) throws IOException { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/json"); + response.setHeader("Location", finalLocation); + PrintWriter writer = response.getWriter(); + writer.write("{\"location\": \""); + writer.write(finalLocation); + writer.write("\"}"); + writer.close(); + } + + private static boolean isJsonEnabled(HttpServletRequest request) { + return "true".equals(request.getParameter(JSONValidationInterceptor.VALIDATE_JSON_PARAM)); + } + + private static boolean isValidateOnly(HttpServletRequest request) { + return "true".equals(request.getParameter(JSONValidationInterceptor.VALIDATE_ONLY_PARAM)); + } +} http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/plugins/json/src/main/java/org/apache/struts2/json/JSONValidationInterceptor.java ---------------------------------------------------------------------- diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONValidationInterceptor.java b/plugins/json/src/main/java/org/apache/struts2/json/JSONValidationInterceptor.java index f895053..0fc7902 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/JSONValidationInterceptor.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONValidationInterceptor.java @@ -61,6 +61,8 @@ import java.util.Map; * <p>If the request has a parameter 'struts.validateOnly' execution will return after * validation (action won't be executed).</p> * + * <p>If 'struts.validateOnly' is set to false you may want to use {@link JSONActionRedirectResult}.</p> + * * <p>A request parameter named 'struts.enableJSONValidation' must be set to 'true' to * use this interceptor</p> * @@ -72,8 +74,8 @@ public class JSONValidationInterceptor extends MethodFilterInterceptor { private static final Logger LOG = LogManager.getLogger(JSONValidationInterceptor.class); - private static final String VALIDATE_ONLY_PARAM = "struts.validateOnly"; - private static final String VALIDATE_JSON_PARAM = "struts.enableJSONValidation"; + static final String VALIDATE_ONLY_PARAM = "struts.validateOnly"; + static final String VALIDATE_JSON_PARAM = "struts.enableJSONValidation"; private static final String NO_ENCODING_SET_PARAM = "struts.JSONValidation.no.encoding"; private static final String DEFAULT_ENCODING = "UTF-8"; http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/plugins/json/src/main/resources/struts-plugin.xml ---------------------------------------------------------------------- diff --git a/plugins/json/src/main/resources/struts-plugin.xml b/plugins/json/src/main/resources/struts-plugin.xml index 5abfbea..58c06c7 100644 --- a/plugins/json/src/main/resources/struts-plugin.xml +++ b/plugins/json/src/main/resources/struts-plugin.xml @@ -9,6 +9,7 @@ <result-types> <result-type name="json" class="org.apache.struts2.json.JSONResult"/> + <result-type name="jsonActionRedirect" class="org.apache.struts2.json.JSONActionRedirectResult"/> </result-types> <interceptors> http://git-wip-us.apache.org/repos/asf/struts/blob/cb118984/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java ---------------------------------------------------------------------- diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java new file mode 100644 index 0000000..c98e41a --- /dev/null +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java @@ -0,0 +1,105 @@ +package org.apache.struts2.json; + +import org.apache.struts2.StrutsStatics; +import org.apache.struts2.StrutsTestCase; +import org.apache.struts2.dispatcher.mapper.DefaultActionMapper; +import org.apache.struts2.views.util.DefaultUrlHelper; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.mock.MockActionInvocation; +import com.opensymphony.xwork2.mock.MockActionProxy; +import com.opensymphony.xwork2.util.ValueStack; + +public class JSONActionRedirectResultTest extends StrutsTestCase { + + MockActionInvocation invocation; + MockHttpServletResponse response; + MockServletContext servletContext; + ActionContext context; + ValueStack stack; + MockHttpServletRequest request; + + public void testNormalRedirect() throws Exception { + JSONActionRedirectResult result = new JSONActionRedirectResult(); + result.setActionName("targetAction"); + result.setActionMapper(new DefaultActionMapper()); + result.setUrlHelper(new DefaultUrlHelper()); + + Object action = new Object(); + stack.push(action); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String content = response.getContentAsString(); + assertEquals("", content); + String location = response.getHeader("Location"); + assertEquals("/targetAction.action", location); + assertEquals(302, response.getStatus()); + } + + public void testJsonRedirect() throws Exception { + JSONActionRedirectResult result = new JSONActionRedirectResult(); + result.setActionName("targetAction"); + result.setActionMapper(new DefaultActionMapper()); + result.setUrlHelper(new DefaultUrlHelper()); + + request.setParameter("struts.enableJSONValidation", "true"); + request.setParameter("struts.validateOnly", "false"); + + Object action = new Object(); + stack.push(action); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String content = response.getContentAsString(); + assertEquals("{\"location\": \"/targetAction.action\"}", content); + assertEquals(200, response.getStatus()); + } + + public void testValidateOnlyFalse() throws Exception { + JSONActionRedirectResult result = new JSONActionRedirectResult(); + result.setActionName("targetAction"); + result.setActionMapper(new DefaultActionMapper()); + result.setUrlHelper(new DefaultUrlHelper()); + + request.setParameter("struts.enableJSONValidation", "true"); + request.setParameter("struts.validateOnly", "true"); + + Object action = new Object(); + stack.push(action); + + this.invocation.setAction(action); + result.execute(this.invocation); + + String content = response.getContentAsString(); + assertEquals("", content); + String location = response.getHeader("Location"); + assertEquals("/targetAction.action", location); + assertEquals(302, response.getStatus()); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + this.response = new MockHttpServletResponse(); + this.request = new MockHttpServletRequest(); + this.context = ActionContext.getContext(); + this.context.put(StrutsStatics.HTTP_RESPONSE, this.response); + this.context.put(StrutsStatics.HTTP_REQUEST, this.request); + this.stack = context.getValueStack(); + this.servletContext = new MockServletContext(); + this.context.put(StrutsStatics.SERVLET_CONTEXT, this.servletContext); + this.invocation = new MockActionInvocation(); + this.invocation.setInvocationContext(this.context); + this.invocation.setStack(this.stack); + MockActionProxy mockActionProxy = new MockActionProxy(); + mockActionProxy.setConfig(new ActionConfig.Builder(null, null, null).build()); + this.invocation.setProxy(mockActionProxy); + } +}