How do I validate a primefaces tree <p:tree>
It appears that primefaces <p:tree>
is not an EditableValueHolder, even though it offers the ability to make the tree selectable. To me this seems like the very definition of EditableValueHolder
as it both holds values (the list of nodes that are selected) and is editable (you can change the selection). In making the tree selectable, it basically turns it into a selectOneXxx/selectManyXxx. This is the fashion in which I use this widget. However, not being an EditableValueHolder, I cannot attach a validator to it directly. I could add validation to the form submission action with an actionListener
but then it is out of the appropriate lifecycle phase and is much more difficult to get at the UITree
component to check for attributes like the i18n message for failed validation. Has anyone dealt with this before? What do you do?
---------- EDIT ----------
I found an issue posted in the primefaces bug tracker that seems releated:
http://code.google.com/p/primefaces/issues/detail?id=4137
And a forum post:
http://forum.primefaces.org/viewtopic.php?f=3&t=22340
---------- EDIT ----------
This is the solution I came up with. Some of the jQuery is pretty hairy as it uses server side el to generate the client side javascript. But for the most part it works. Just have to figure out why an empty array skips validation... but thats another story.
<h:panelGroup id="pnpCois" styleClass="pnp-input-group pnp-cois">
<h:outputLabel for="inputCois"
value="#{i18n['communities-of-interest']}" />
<p:tree id="inputCois"
value="#{subscriptions.selected.coiTreeRootNode}" var="node"
selectionMode="checkbox"
selection="#{subscriptions.selected.selectedCoiNodes}">
<p:ajax event="select" process="@this :#{component.clientId}_validator" update="@this"
onstart="$('##{component.clientId}_validator'.replace(':',':')).val($('##{component.clientId}_selection'.replace(':',':')).val());" />
<p:ajax event="unselect" process="@this :#{component.clientId}_validator" update="@this"
onstart="$('##{component.clientId}_validator'.replace(':',':')).val($('##{component.clientId}_selection'.replace(':',':')).val());" />
<p:treeNode>
<h:outputText value="#{node}" />
</p:treeNode>
</p:tree>
<h:inputHidden id="inputCois_validator">
<f:converter converterId="asias.stringCsvToArray" />
<f:validator validatorId="asias.atLeastOneSelected" />
<f:attribute name="atLeastOneSelectedMessage"
value="#{i18n['at-least-one-coi-must-be-selected']}" />
</h:inputHidden>
</h:panelGroup>
---------- EDIT ----------
After working through some suggestions with BalusC, I think I'm gonna give up on <p:tree>
and find another way... :(
You can trick it with a required hidden input field whose value is altered on node click. You can use the selections
property of the <p:tree>
widget variable to get the available selections as an array.
Eg
<h:form id="form">
<p:tree widgetVar="tree"
onNodeClick="$('#form:treeSelections').val(tree.selections.length != 0 ? 'ok' : '')">
...
</p:tree>
<h:inputHidden id="treeSelections" required="true"
requiredMessage="Please select at least one tree node" />
<p:message for="treeSelections" />
</h:form>
The 'ok'
value is purely arbitrary. The point is that the hidden field is filled, so that the required
validator doesn't get triggered.
Bear with me, this is a long answer...
Since primefaces tree is not an EditableValueHolder
it cannot be validated during the standard process validations phase of the JSF lifecycle (without some major hacking). And to the best of my ability, I was not able to patch the primefaces tree code to make it an EditableValueHolder
(the tree does not get rendered according to the selected values, but according to the state of the nodes backing the tree). Given these constraints, the only solutions are create my own tree component (I don't have the time), use a different component (a tree fit best), or validate in the invoke application phase.
I chose the 3rd solution, and in doing so, tried to make it as similar to regular validation as possible. The main idea is, use an actionListener
that gets fired first (before any other actionListener
s or the main action (save the form) to process validations. If validations failed, I add information about the failure to the component in custom attributes, call facesContext.validationFailed()
so I can skip the action, then add a preRenderView
system event listener to modify the components according to their validation state before the render response phase. This is done in a fashion that allows you to still specify the validation in the same fashion using a custom component instead of an <f:validator>
. Here is the code:
web.xml:
...
<context-param>
<param-name>javax.faces.FACELETS_LIBRARIES</param-name>
<param-value>/WEB-INF/somenamespace.taglib.xml</param-value>
</context-param>
...
somenamespace.taglib.xml:
<facelet-taglib version="2.0" 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-facelettaglibrary_2_0.xsd">
<namespace>http://ns.my.com/ui/extensions</namespace>
<tag>
<description><![CDATA[
Add an actionListener validator to a component
]]></description>
<tag-name>actionListenerValidator</tag-name>
<handler-class>com.my.ns.actionlistenervalidator.ActionListenerValidatorHandler</handler-class>
<attribute>
<description><![CDATA[
The validatorId.
]]></description>
<name>validatorId</name>
<type>java.lang.String</type>
</attribute>
<attribute>
<description><![CDATA[
A ValueExpression that evaluates to an instance of Validator.
]]></description>
<name>binding</name>
<type>javax.el.ValueExpression</type>
</attribute>
<attribute>
<description><![CDATA[
The styleClass added to the end of the component style class when a validation error occurs
]]></description>
<name>errorStyleClass</name>
<type>java.lang.String</type>
</attribute>
</tag>
</facelet-taglib>
ActionListenerHandler.java:
package com.my.ns.actionlistenervalidator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.component.UIComponent;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributes;
import javax.faces.view.facelets.TagConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sun.faces.facelets.tag.TagHandlerImpl;
public class ActionListenerValidatorHandler extends TagHandlerImpl {
private static Logger logger = LoggerFactory.getLogger( ActionListenerValidatorHandler.class );
public static enum AttributeKeys {
errorStyleClass("hack.jsf.actionlistenervalidator.errorStyleClass"),
messages("hack.jsf.actionlistenervalidator.messages"),
valid("hack.jsf.actionlistenervalidator.valid"),
validators("hack.jsf.actionlistenervalidator.validators");
private String key;
private AttributeKeys( String key ) {
this.key = key;
}
public String getKey() {
return key;
}
}
public ActionListenerValidatorHandler( TagConfig config ) {
super( config );
}
@Override
public void apply( FaceletContext ctx, UIComponent parent ) throws IOException {
ActionListenerValidatorWrapper validator = new ActionListenerValidatorWrapper( ctx.getFacesContext(),
tagAttributesToMap( ctx, this.tag.getAttributes() ) );
logger.trace( "adding actionListener validator {} to {}", validator, parent );
@SuppressWarnings( "unchecked" )
List<ActionListenerValidatorWrapper> validators = (List<ActionListenerValidatorWrapper>) parent.getAttributes().get( AttributeKeys.validators.getKey() );
if ( validators == null ) {
validators = new ArrayList<ActionListenerValidatorWrapper>();
parent.getAttributes().put( AttributeKeys.validators.getKey(), validators );
}
validators.add( validator );
}
private Map<String, Object> tagAttributesToMap( FaceletContext ctx, TagAttributes tagAttributes ) {
Map<String, Object> map = new HashMap<String, Object>();
for ( TagAttribute attribute : tagAttributes.getAll() ) {
map.put( attribute.getLocalName(), attribute.getValue( ctx ) );
}
return map;
}
}
ActionListenerValidatorWrapper.java:
package com.my.ns.actionlistenervalidator;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import javax.el.ELContext;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import javax.faces.view.facelets.FaceletException;
import com.sun.faces.el.ELUtils;
public class ActionListenerValidatorWrapper {
private Validator validator;
private String errorStyleClass;
public ActionListenerValidatorWrapper( FacesContext context, Map<String, Object> attributes ) {
String binding = (String) attributes.get( "binding" );
String validatorId = (String) attributes.get( "validatorId" );
if ( binding != null ) {
ExpressionFactory factory = context.getApplication().getExpressionFactory();
ELContext elContext = context.getELContext();
ValueExpression valueExpression = factory.createValueExpression(
elContext, binding, String.class );
this.validator = (Validator) ELUtils.evaluateValueExpression( valueExpression, context.getELContext() );
}
else if ( validatorId != null ) {
this.validator = context.getApplication().createValidator( validatorId );
this.errorStyleClass = (String) attributes.get( "errorStyleClass" );
// inject all attributes
for ( Method method : validator.getClass().getMethods() ) {
String methodName = method.getName();
Class<?>[] types = method.getParameterTypes();
if ( methodName.startsWith( "set" ) && types.length == 1 ) {
String property = Character.toLowerCase( methodName.charAt( 3 ) ) + methodName.substring( 4 );
if ( attributes.containsKey( property ) ) {
// convert value type
Object value = null;
if ( types[0] == Integer.TYPE ) {
value = intValue( context, attributes.get( property ) );
}
else {
value = attributes.get( property );
}
// call setter
try {
method.invoke( validator, value );
}
catch ( IllegalArgumentException e ) {
throw new FaceletException( e );
}
catch ( IllegalAccessException e ) {
throw new FaceletException( e );
}
catch ( InvocationTargetException e ) {
throw new FaceletException( e );
}
}
}
}
}
else {
throw new FaceletException( "ActionListenerValidator requires either validatorId or binding" );
}
}
@Override
public boolean equals( Object otherObj ) {
if ( !(otherObj instanceof ActionListenerValidatorWrapper) ) {
return false;
}
ActionListenerValidatorWrapper other = (ActionListenerValidatorWrapper) otherObj;
return (this.getValidator().equals( other.getValidator() ))
&& (this.getErrorStyleClass().equals( other.getErrorStyleClass() ));
}
public String getErrorStyleClass() {
return errorStyleClass;
}
public Validator getValidator() {
return validator;
}
@Override
public int hashCode() {
int hashCode = (getValidator().hashCode()
+ getErrorStyleClass().hashCode());
return (hashCode);
}
private Integer intValue( FacesContext context, Object value ) {
ExpressionFactory factory = context.getApplication().getExpressionFactory();
ELContext elContext = context.getELContext();
ValueExpression valueExpression = factory.createValueExpression(
elContext, value.toString(), String.class );
if ( !valueExpression.isLiteralText() ) {
return ((Number) ELUtils.evaluateValueExpression( valueExpression, elContext )).intValue();
}
else {
return Integer.valueOf( valueExpression.getExpressionString() );
}
}
@Override
public String toString() {
return validator.getClass().getName();
}
public void validate( FacesContext context, UIComponent component, Object value ) throws ValidatorException {
validator.validate( context, component, value );
}
}
ActionListenerValidatorManager.java:
package com.my.ns.actionlistenervalidator;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.ViewScoped;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.validator.ValidatorException;
import com.my.ns.controller.MyBean;
import com.my.ns.actionlistenervalidator.ActionListenerValidatorHandler.AttributeKeys;
import org.primefaces.component.tree.Tree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ManagedBean
@ViewScoped
public class ActionListenerValidatorManager implements Serializable {
private static final long serialVersionUID = -696487579396819893L;
private static Logger logger = LoggerFactory.getLogger( ActionListenerValidatorManager.class );
@ManagedProperty( "#{myBean}" )
private MyBean myBean;
private void addValidationToComponent( Map<String, Object> attributes, Collection<FacesMessage> facesMessages, Set<String> errorStyleClasses ) {
attributes.put( AttributeKeys.valid.getKey(), false );
attributes.put( AttributeKeys.messages.getKey(), facesMessages );
StringBuilder builder = new StringBuilder();
if ( errorStyleClasses != null ) {
for ( String styleClass : errorStyleClasses ) {
builder.append( styleClass );
}
attributes.put( AttributeKeys.errorStyleClass.getKey(), builder.toString() );
}
}
public void applyValidationStateToComponentTreeBecausePrimefacesDidntMakeTreeAnEditableValueHolder( ComponentSystemEvent event ) {
applyValidationStateToComponentTree( FacesContext.getCurrentInstance() );
}
private void applyValidationStateToComponentTree( FacesContext context ) {
UIViewRoot viewRoot = context.getViewRoot();
logger.trace( "pre render view for {}", viewRoot );
viewRoot.visitTree( VisitContext.createVisitContext( context ),
new VisitCallback() {
@Override
public VisitResult visit( VisitContext context, UIComponent component ) {
Map<String, Object> attributes = component.getAttributes();
if ( attributes.containsKey( AttributeKeys.valid.getKey() ) &&
!((Boolean) attributes.get( AttributeKeys.valid.getKey() )) ) {
// validation state
if ( component instanceof EditableValueHolder ) {
((EditableValueHolder) component).setValid( false );
}
// validation messages
FacesContext facesContext = context.getFacesContext();
@SuppressWarnings( "unchecked" )
List<FacesMessage> messages = (List<FacesMessage>) attributes.get( AttributeKeys.messages.getKey() );
if ( messages != null ) {
for ( FacesMessage message : messages ) {
facesContext.addMessage( component.getClientId(), message );
}
}
// style class
String errorStyleClass = (String) attributes.get( AttributeKeys.errorStyleClass.getKey() );
if ( errorStyleClass != null ) {
String styleClass = (String) attributes.get( "styleClass" );
styleClass = styleClass == null ? errorStyleClass : styleClass + " " + errorStyleClass;
attributes.put( "styleClass", styleClass );
}
}
return VisitResult.ACCEPT;
}
} );
}
private void clearValidationFromTree( FacesContext context, UIComponent component ) {
component.visitTree( VisitContext.createVisitContext( context ),
new VisitCallback() {
@Override
public VisitResult visit( VisitContext context, UIComponent target ) {
clearValidationFromComponent( target.getAttributes() );
return VisitResult.ACCEPT;
}
} );
}
private void clearValidationFromComponent( Map<String, Object> attributes ) {
if ( attributes.containsKey( AttributeKeys.validators.getKey() ) ) {
String errorStyleClass = (String) attributes.get( AttributeKeys.errorStyleClass.getKey() );
if ( errorStyleClass != null ) {
String styleClass = (String) attributes.get( "styleClass" );
styleClass = styleClass.replace( errorStyleClass, "" );
attributes.put( "styleClass", styleClass );
}
attributes.remove( AttributeKeys.valid.getKey() );
attributes.remove( AttributeKeys.messages.getKey() );
attributes.remove( AttributeKeys.errorStyleClass.getKey() );
}
}
private Object getValue( FacesContext facesContext, UIComponent component ) {
Object value = null;
if ( component instanceof EditableValueHolder ) {
value = ((EditableValueHolder) component).getValue();
}
else if ( component instanceof Tree ) {
value = myBean.getSelectedIds();
}
return value;
}
public void setMyBean( MyBean myBean ) {
this.myBean = myBean;
}
private void validate( FacesContext context ) {
logger.trace( "entering validation" );
final List<String> validationFailed = new ArrayList<String>();
UIViewRoot viewRoot = context.getViewRoot();
viewRoot.visitTree( VisitContext.createVisitContext( context ),
new VisitCallback() {
@Override
public VisitResult visit( VisitContext context, UIComponent component ) {
if ( !component.isRendered() ) {
// remove all validation from subtree as validation
// is not performed unless the component is
// rendered.
clearValidationFromTree( context.getFacesContext(), component );
return VisitResult.REJECT;
}
Map<String, Object> attributes = component.getAttributes();
if ( attributes.containsKey( AttributeKeys.validators.getKey() ) ) {
Object value = getValue( context.getFacesContext(), component );
boolean valid = true;
Collection<FacesMessage> facesMessages = null;
Set<String> errorStyleClasses = null;
@SuppressWarnings( "unchecked" )
List<ActionListenerValidatorWrapper> validators =
(List<ActionListenerValidatorWrapper>) attributes.get( AttributeKeys.validators.getKey() );
for ( ActionListenerValidatorWrapper validator : validators ) {
try {
validator.validate( context.getFacesContext(), component, value );
}
catch ( ValidatorException validatorException ) {
valid = false;
Collection<FacesMessage> innerMessages = validatorException.getFacesMessages();
if ( innerMessages == null ) {
FacesMessage innerMessage = validatorException.getFacesMessage();
if ( innerMessage != null ) {
innerMessages = Arrays.asList( new FacesMessage[] { innerMessage } );
}
}
if ( facesMessages == null ) {
facesMessages = new ArrayList<FacesMessage>();
}
facesMessages.addAll( innerMessages );
String errorStyleClass = validator.getErrorStyleClass();
if ( errorStyleClass != null ) {
if ( errorStyleClasses == null ) {
errorStyleClasses = new TreeSet<String>();
}
errorStyleClasses.add( errorStyleClass );
}
}
}
if ( valid ) {
// remove previous validation state
clearValidationFromComponent( attributes );
}
else {
// add validation state
addValidationToComponent( attributes, facesMessages, errorStyleClasses );
validationFailed.add( "Yes, it did, but cant update final boolean so we use a list" );
}
}
return VisitResult.ACCEPT;
}
} );
if ( validationFailed.size() > 0 ) {
context.validationFailed();
}
}
public void validateThisFormBecausePrimefacesDidntMakeTreeAnEditableValueHolder( ActionEvent event ) {
validate( FacesContext.getCurrentInstance() );
}
}
And finally the page that uses it:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui"
xmlns:fn="http://java.sun.com/jsp/jstl/functions"
xmlns:myns="http://ns.my.com/ui/extensions">
<h:head />
<h:body>
<f:event type="preRenderView"
listener="#{actionListenerValidatorManager.applyValidationStateToComponentTreeBecausePrimefacesDidntMakeTreeAnEditableValueHolder}" />
...
<h:panelGroup id="treeGroup">
<h:outputLabel for="treeInput"
value="#{i18n['my-tree']}" />
<p:tree id="treeInput"
value="#{myBean.treeRootNode}" var="node"
selectionMode="checkbox"
selection="#{myBean.selectedNodes}">
<pe:actionListenerValidator
validatorId="javax.faces.Required"
errorStyleClass="ui-state-error" />
<p:treeNode>
<h:outputText value="#{node}" />
</p:treeNode>
</p:tree>
</h:panelGroup>
...
</h:body>
</html>
I know this is not a cut/paste type answer, but it fully outlines the process. The main benefit of this approach is that it feels the same as standard validation in the way it is used and the way it is processed. Plus it leverages existing validators. If anyone else is stuck using <p:tree>
and must validate the selection, I hope this helps...
上一篇: 滞后于data.table R