Repository: struts Updated Branches: refs/heads/support-2-3 15c79e139 -> 2a6acabe5
WW-4599: backport JSONActionRedirectResult to struts 2.3 Project: http://git-wip-us.apache.org/repos/asf/struts/repo Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/69bc2947 Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/69bc2947 Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/69bc2947 Branch: refs/heads/support-2-3 Commit: 69bc2947c17188bda2502a5bfe1c19e427583581 Parents: 15c79e1 Author: cnenning <cnenn...@apache.org> Authored: Mon Jan 11 14:36:32 2016 +0100 Committer: cnenning <cnenn...@apache.org> Committed: Fri Jan 29 13:53:31 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 | 1 - .../WEB-INF/validation/ajaxFormSubmit.jsp | 177 +++++++++++++++++++ .../validation/ajaxFormSubmitSuccess.jsp | 17 ++ .../main/webapp/WEB-INF/validation/index.jsp | 2 + .../struts2/json/JSONActionRedirectResult.java | 71 ++++++++ .../struts2/json/JSONValidationInterceptor.java | 6 +- .../json/src/main/resources/struts-plugin.xml | 1 + .../json/JSONActionRedirectResultTest.java | 105 +++++++++++ 15 files changed, 735 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/struts/blob/69bc2947/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/69bc2947/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/69bc2947/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 ee40dc9..fc59210 100755 --- a/apps/showcase/src/main/resources/struts-validation.xml +++ b/apps/showcase/src/main/resources/struts-validation.xml @@ -29,6 +29,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/69bc2947/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/69bc2947/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/69bc2947/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/69bc2947/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/69bc2947/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 1ca02cc..684a0f1 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp @@ -246,7 +246,6 @@ </li> <li><s:a value="/interactive/index.action">Interactive Demo</s:a></li> </ul> - <ul class="nav pull-right"> <li class="dropdown last"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-flag"></i> Help<b http://git-wip-us.apache.org/repos/asf/struts/blob/69bc2947/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/69bc2947/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/69bc2947/apps/showcase/src/main/webapp/WEB-INF/validation/index.jsp ---------------------------------------------------------------------- diff --git a/apps/showcase/src/main/webapp/WEB-INF/validation/index.jsp b/apps/showcase/src/main/webapp/WEB-INF/validation/index.jsp index 96a331f..00c1579 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/validation/index.jsp +++ b/apps/showcase/src/main/webapp/WEB-INF/validation/index.jsp @@ -31,6 +31,7 @@ <s:url var="clientSideValidationUrl" action="clientSideValidationExample" namespace="/validation"/> <s:url var="backToShowcase" action="showcase" namespace="/"/> <s:url var="storeMessageAcrossRequestExample" namespace="/validation" action="storeErrorsAcrossRequestExample"/> + <s:url var="ajaxFormSubmitAction" namespace="/validation" action="ajaxFormSubmit!input"/> <ul> <li><s:a href="%{fieldValidatorUrl}">Field Validators</s:a></li> @@ -42,6 +43,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="%{ajaxFormSubmitAction}">AJAX Form Submit</s:a></li> </ul> </div> </div> http://git-wip-us.apache.org/repos/asf/struts/blob/69bc2947/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..f9b28b2 --- /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.dispatcher.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/69bc2947/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 8194205..1c7fd54 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 = LoggerFactory.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/69bc2947/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/69bc2947/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); + } +}