Wednesday, February 29, 2012

JSF 2 - Conditionally Skip Validation

The validated model update is a major foundation of Java Server Faces. It eases the development of complex web forms and together with JPA and Bean Validation it enforces best practices like database consistency and appropriate GUI behaviour, security etc. In JSF additional measures are necessary to skip the basic validation step, for instance in use cases with sub dialogs or wizards.
This article describes, how to update the model but skip the JSF validation in dependence of which button is clicked.

Why would anyone like to skip validation?

I give you one example. We often build heavy weight forms in our business apps - so we are not the cool guys in this cartoon. This are real business requirements and even if we liked to we cannot always change customers use cases.
Such forms not only contain basic input fields, selections etc., but there are also components like Add / Select..., where you go to a searchable list, select one or multiple items and come back.
This is a typical requirement in modern web application with rich visuals - so open a JavaScript based modal subdialog and you are done!? We mainly develop application for the e-Government. Currently this implies in Germany:
All functions must work with disabled JavaScript.
You find this very often in tenders for eGov business apps and it originates in the BITV history and misinterpretations. So we have to solve this without JavaScript, possible steps:
  • action Add / Select..., we must temporarily leave the form
  • apply model updates without validation: ignore invalid input and empty required fields
  • go to a subdialog, come back with the selected data
  • finally Save, but with complete form validation

Conditional Validation - First Considerations

The often recommended <h:commandButton immediate="true"> skips validation but doesn't help here, because no model updates are done either. You can use this feature for Cancel / Reset actions, but not for this use case.

The first solution that comes into mind is to add a parameter to the command action, which is possible since JSF 2:
<h:commandButton action="#{bean.addItem"} value="Add...">
   <f:param name="skipValidation" value="true"/>
</h:commandButton>
Other new JSF 2 features that help here are f:validateBean and f:validateRequired with a dynamic disabled attribute or @SkipValidation from ExtVal. I see multiple problems with this solution:
  • JavaScript necessary
    • With basic JSF components it was always quite clear which components can be used without JS, h:commandButton was one of them. But the new feature f:param changes this.
  • not secure
    • you can skip validation for all actions, even for unintended like saving:
    • simply add skipValidation=true as parameter via a tool, e.g.:
    • with addons like Firefox Tamper Data you can intercept and change POSTs
  • not a global solution
    • if you need this feature for many forms and components you have to duplicate code

JavaScript - resulting HTML for above example:
<input type="submit" name="liste:j_idt90" value="Add..." class="button"
    onclick="mojarra.jsfcljs(document.getElementById('liste'),{'liste:j_idt90':'liste:j_idt90','skipValidation':'true'},'');return false" />

 

Working Solution

The idea with custom validators is good but we need to find an alternative trigger and provide a global solution for this. Even if I don't like the JS-nature of h:commandButton parameters I understand it's technical reasons. Where can we add information to a button, so that the framework can decide if this should be validated or not?

We define a rule:

All commandButton ids that require validation start with "do" - for instance "doSave".

<h:commandButton action="#{bean.doSave}" id="doSave" value="Save" />
That means, actions with "do" as prefix behave like regular JSF, actions without this prefix are not validated.
If the form has the id="edit" then JSF generates this HTML:
<input id="edit:doSave" type="submit" name="edit:doSave" value="Save" />
If you look into the POST, again with Tamper Data:

The advantages of this rule:
  • we can use this "do" as global (or local) validation trigger (see ValidatorUtil below)
  • you cannot change the behaviour, if you add edit:Save=Save instead of edit:doSave=Save nothing happens, JSF will not execute the action
  • we cannot change the rule to something like "actions with <whatever>" are not validated, again you could add POST attributes that prevent validation for unintended cases

Implementation


The implementation consists out of an utility class, two slightly enhanced global validator classes and the validator declaration in app/src/main/webapp/WEB-INF/faces-config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
 version="2.0">
...
 <validator>
  <validator-id>javax.faces.Bean</validator-id>
  <validator-class>app.controller.util.validator.SkipBeanValidator</validator-class>
 </validator>
 <validator>
  <validator-id>javax.faces.Required</validator-id>
  <validator-class>app.controller.util.validator.SkipRequiredValidator</validator-class>
 </validator>
...
</faces-config>

Enhanced global Bean Validator app.controller.util.validator.SkipBeanValidator:
package app.controller.util.validator;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.BeanValidator;

public class SkipBeanValidator extends BeanValidator {

 @Override
 public void validate(final FacesContext context, final UIComponent component, final Object value) {
  if (ValidatorUtil.check(context)) {
   super.validate(context, component, value);
  }
 }

}

Enhanced global Required Validator app.controller.util.validator.SkipRequiredValidator:
package app.controller.util.validator;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.RequiredValidator;

public class SkipRequiredValidator extends RequiredValidator {

 @Override
 public void validate(final FacesContext context, final UIComponent component, final Object value) {
  if (ValidatorUtil.check(context)) {
   super.validate(context, component, value);
  }
 }

}

Utility class app.controller.util.validator.ValidatorUtil:
package app.controller.util.validator;

import java.util.Map;

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;

public final class ValidatorUtil {

 private static final String VALIDATE = "VALIDATE";

 public static boolean check() {
  return check(FacesContext.getCurrentInstance());
 }

 public static boolean check(final FacesContext context) {
  final ExternalContext externalContext = context.getExternalContext();
  final Object validate = externalContext.getRequestMap().get(VALIDATE);
  if (validate != null) {
   return (Boolean) validate;
  }
  for (final Map.Entry<String, String[]> requestParameters : externalContext.getRequestParameterValuesMap()
    .entrySet()) {
   final String key = requestParameters.getKey();
   if (key.contains(":do")) {
    externalContext.getRequestMap().put(VALIDATE, Boolean.TRUE);
    return true;
   }
  }
  externalContext.getRequestMap().put(VALIDATE, Boolean.FALSE);
  return false;
 }

}

 

Conclusions

Please feel free to comment if you recognize problems with this solution or suggest alternatives. Even though I have some years under the belt with JSF - you should always be careful with such framework adaptions. Automatic validation is a foundation of JSF functionality and security. JSF is very extensible - but not all what can be done should be done. In feature JSF releases I would like to find a ready to use solution for this problem.

11 comments:

  1. Nice post and Nice blog too. thanks for sharing.

    Javin

    ReplyDelete
  2. Great post. I'd also like to add that for my scenario I added a filter that strips the request of all user provided parameters so that the security concern is removed because untrusted user data cannot be provided when validation is skipped. This may not match what you needed but given that it eliminates the security concern I'd love to see this feature in future JSF implementations.

    ReplyDelete
    Replies
    1. Hi,

      thx, may be you have some code or specific examples for this?

      Yes we skip validation, but in this case we _always_ don't really apply this to the persistent model. That is very important!

      The HTML encoding is done anyway, so no XSS, validation or not.
      Even with persistence no SQL Injection is possible because we exclusively use Prepared Statements (via JPA).

      Cheers,
      André

      Delete
  3. It is a useful solution, I was facing a similar problem!
    Thanks!
    Regards
    Angel S.

    ReplyDelete
  4. don't we need to call those validators in .xhtml file ? how they get fired ?

    ReplyDelete
  5. I've tried but it didn't works, i create the 3 clases into the same package and added the two validators into my faces-config.xml file, the validation is fired always regardless the id of the commandbutton

    may not work because I'm using jsf with spring??

    ReplyDelete
  6. Same problem as Elianor. I'm using tomcat7, mojarra2.2, not using spring. The vaidators are not used. I'd guess that the validator-id elements aren't matching but I don't know what else to try.
    Thanks
    David M

    ReplyDelete
    Replies
    1. How did you come up with those validator-id elements? Aren't they usually strings that match id arguments in components in the xhtml?
      Dave M

      Delete
    2. OK. Now I have the same problem as Elianor. Before, neither custom validator was being called. I had to add the following to faces-config.xml



      javax.faces.Bean
      javax.faces.Required



      Now I can see SkipRequiredValidator.validate being called. I have not seen SkipBeanValidator.validate being called. I can see that the call to super.validate is skipped as appropriate but the form still fails required attribute validation.

      Hopefully, Dave M

      Delete
    3. The embedded xml didn't fare too well. The javax.faces.Bean and javax.faces.Required were put inside default-validators element inside application element inside faces-config element

      Delete
  7. This comment has been removed by the author.

    ReplyDelete