This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch json-validation in repository https://gitbox.apache.org/repos/asf/struts-site.git
commit e20f5ddd4cdc1a54acbb057d31eaff51c7eb7afe Author: Lukasz Lenart <lukaszlen...@apache.org> AuthorDate: Wed May 15 10:15:58 2019 +0200 Adds missing JSON validation page --- pom.xml | 2 +- source/plugins/json/index.md | 2 +- source/plugins/json/json-validation.md | 507 +++++++++++++++++++++++++ source/plugins/json/struts2-ajax-vali-flow.png | Bin 0 -> 54364 bytes 4 files changed, 509 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1984c3a..caf6162 100644 --- a/pom.xml +++ b/pom.xml @@ -101,7 +101,7 @@ <argument>https://cwiki.apache.org/confluence</argument> <argument>+gfm</argument> <argument>true</argument> - <argument>13859</argument> + <argument>13850</argument> </arguments> </configuration> </execution> diff --git a/source/plugins/json/index.md b/source/plugins/json/index.md index 43e016d..14d158d 100644 --- a/source/plugins/json/index.md +++ b/source/plugins/json/index.md @@ -46,7 +46,7 @@ The `setArray` method can take as parameter either a `List`, or any numeric arra `root` attribute must be set on the `JSONInterceptor` when dealing with JSON array. -This plugin also provides _AJAX Validation_. +This plugin also provides [JSON Validation](json-validation). ## Installation diff --git a/source/plugins/json/json-validation.md b/source/plugins/json/json-validation.md new file mode 100644 index 0000000..0ab0ca4 --- /dev/null +++ b/source/plugins/json/json-validation.md @@ -0,0 +1,507 @@ +--- +layout: plugin +title: JSON plugin +--- + +# JSON Ajax Validation +{:.no_toc} + +* Will be replaced with the ToC, excluding a header +{:toc} + +## Description + +Struts provides [client side validation](../../core-developers/client-side-validation) (using JavaScript) for a few validators. Using AJAX +validation, all _validators_ available to the application on the server side can be used without forcing the page to reload, just to show +validation errors. AJAX validation has a server side, which is in included in [JSON Plugin](index) (an interceptor and a result). Client +side must be handled by applications themself. One reason for that is there are too many JavaScript frameworks and libraries. Struts has +no preference which of them you use. Previous versions of Struts included a client side which was relying on the Dojo JS framework and was +located in Struts Dojo plugin. That has been deprecated for a long time and was eventually removed. + +## Example + +This example is taken from the Struts showcase application. + +### Create the action class + +```java +public class AjaxFormSubmitAction extends ActionSupport { + 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; + } +} +``` + +### Map the Action + +Note that is is not necessary when using [Convention Plugin](../convention/) + +```xml +<!DOCTYPE struts PUBLIC + "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN" + "http://struts.apache.org/dtds/struts-2.5.dtd"> + +<struts> + <package> + <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> + </package> +</struts> +``` + +AJAX validation is performed by the _jsonValidation_ interceptor. This interceptor is included in the _jsonValidationWorkflowStack_, +and is required in order to perform AJAX validation. Normal results(`input`, `success`, etc) should be provided for the action in the case +that someone tries to access the action directly, in which case normal validation will be triggered. So, how does the _jsonValidation_ +know that it must perform AJAX validation vs regular validation? We will see that in a minute, but you don't need to know that in order +to use AJAX validation. Same applies for specialized Redirect Result Type _jsonActionRedirect_. + +### Create the JSP + +```html +<%@taglib prefix="s" uri="/struts-tags" %> +<html> +<head> + <title>Struts2 Showcase - Validation - AJAX Form Submit</title> + <s:head theme="xhtml"/> +</head> +<body> +<div class="page-header"> + <h1>AJAX Form Submit</h1> +</div> + <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> +</body> +</html> +``` + +Things to note on this JSP: + +- The _form_ tag **does not** have _validate_ set to _true_, which would perform client validation before the AJAX validation. +- It uses a customized theme _ajaxErrorContainers_. The default Struts themes generate HTML-Elements to show validation errors only + if errors are present when page is created on server side. But in order to show validation errors that arrive later via AJAX + it is necessary to have error-container elements in DOM always. + +What happens if validation succeeds? That depends on your request parameters and action configuration. If you are using _jsonActionRedirect_ +result mentioned above the action will be executed while AJAX request is active and respond with JSON providing a new URL to load. +Otherwise the AJAX response will be empty and the form must be submitted a 2nd time but as usual request, not AJAX. + +> Setting _validate_ to _true_ in the _form_ tag will enable client side, JavaScript validation, which can be used along with AJAX validation +> (runs before the AJAX validation). + +### Custom Theme + +In this sample the _custom theme_ is based on _xhtml_ theme. It is required to override 3 FTL files. + +#### theme.properties + +``` +parent = xhtml +``` + +#### actionerror.ftl + +```html +<#-- + 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> +``` + +#### controlfooter.ftl + +```html +${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> +``` + +#### controlheader-core.ftl + +```html + <#-- + 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> +``` + +### CSS + +To show users some nice visual feedback while waiting for AJAX response you can use a little CSS. Remember to include the referenced +_indicator.gif_ . + +```js +.ajaxVisualFeedback { + width: 16px; + height: 16px; + background-image: url('../images/indicator.gif'); + background-repeat: no-repeat; + float: right; +} +``` + +### JavaScript + +Now this is where the magic happens. Here _jQuery_ is used to register an eventhandler which intercepts form submits. It takes +care of hiding validation errors that might be present, submit the form via AJAX and handle JSON responses. + +```js + /** + * Validates 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(); + } + // struts sends status code 400 when validation errors are present + if (xhr.status === 400) { + _handleValidationResult(_form, JSON.parse(xhr.responseText)) + } else { + // a real error occurred -> 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. + */ +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); // use text() for security reasons + 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); // use text() for security reasons + td.append(div); + } + }); + } +} +// register onSubmit handler +$(window).bind('load', function() { + $('form').bind('submit', ajaxFormValidation); +}); +``` + +### How it works + +_jsonValidation_ interceptor must be placed on a stack, following the _validation_ interceptor. The interceptor itself won't perform any +validation, but will check for validation errors on the action being invoked (assuming that the action is ValidationAware). + +If you just want to use AJAX validation, without knowing the implementation details, you can skip this section. + +When the _jsonValidation_ interceptor is invoked, it will look for a parameter named _struts.enableJSONValidation_, this parameter +**must** be set to _true_, otherwise the interceptor won't do anything. Then the interceptor will look for a parameter named _struts.validateOnly_, +if this parameter exists, is set to _true_, and there are validation errors (o action errors) they will be serialized into JSON in the form: + +```json +{ + "errors": ["Global Error 1", "Global Error 2"], + "fieldErrors": { + "field1": ["Field 1 Error 1", "Field 1 Error 2"], + "field1": ["Field 2 Error 1", "Field 2 Error 2"] + } +} +``` + +If the action implements the _ModelDrive_ interface, "model." will be stripped from the field names in the returned JSON. If validation +succeeds (and _struts.validateOnly_ is true), an empty JSON string will be returned: + +```json +{} +``` + +If _struts.validateOnly_ is false the action and result are executed. In this case _jsonActionRedirect_ result is very useful. It creates +a JSON response in the form: + +```json +{"location": "<url to be loaded next>"} +``` + +> Remember to set `struts.enableJSONValidation=true` in the request to enable AJAX validation + +### JSONValidationInterceptor parameters + +The following request parameters can be used to enable exposing validation errors: + +- **struts.enableJSONValidation** - a request parameter must be set to **true** to use this interceptor +- **struts.validateOnly** - If the request has this parameter, execution will return after validation (action won't be executed). + If **struts.validateOnly** is set to false you may want to use _JSONActionRedirectResult_ +- **struts.JSONValidation.no.encoding** - If the request has this parameter set to **true,** the character encoding will **NOT** be set on + the response - is needed in portlet environment + +You can override names of these parameters by specifying the following parameters when setting up a stack: + +- **validateJsonParam** - to override name of **struts.enableJSONValidation**** +- **validateOnlyParam** - to override name of **struts.validateOnly** +- **noEncodingSetParam** - to override name of **struts.JSONValidation.no.encoding** +- **validationFailedStatus** - status to be set on response when there are validation errors, by default **400** + +Parameters overriding is available since Struts 2.5.9 + +## Flow chart of AJAX validation + +Some details are omitted, like results used. + +As explained above: there is a case where form is submitted twice, one time as AJAX with validation only and another time as usual submit. + +![Flow chart ](struts2-ajax-vali-flow.png) + \ No newline at end of file diff --git a/source/plugins/json/struts2-ajax-vali-flow.png b/source/plugins/json/struts2-ajax-vali-flow.png new file mode 100755 index 0000000..8da0331 Binary files /dev/null and b/source/plugins/json/struts2-ajax-vali-flow.png differ