10

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... :(

Lucas
  • 14,227
  • 9
  • 74
  • 124

2 Answers2

5

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.

E.g.

<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.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • is it legal to have a validator on an input without a value binding? – Lucas Jan 17 '13 at 21:41
  • Never mind, stupid question. Of course binding doesn't matter, the component tree is completely separate. – Lucas Jan 17 '13 at 21:47
  • I marked this as the answer because it inspired my solution and is certainly valid. Thank you. I am adding my solution above as a reference. – Lucas Jan 17 '13 at 21:58
  • In the solution above, since tree is not an `EditableValueHolder` it does not keep a submitted value to render upon validation failure so it always renders from the model. When the associated hidden input fails validation due to a change in the tree, the tree reverts to the model value instead of the submitted/changed value. Any idea on how to work around this issue? Its starting to feel like my only options are implement my own tree, or only do validation in the invoke_application phase... – Lucas Jan 18 '13 at 15:41
  • I'm honestly said not exactly sure why you achieved it the way as demonstrated in your question instead of as demonstrated in my answer. I'm a bit missing the point of that. Can you explain that? – BalusC Jan 18 '13 at 15:43
  • The only real difference between your answer and the one in the question, is that in your answer it uses onNodeClick which happens when the node gets clicked, not just the checkbox part. Not a big deal. Also you use the built in required and set the value of the hidden input to an arbitrary value. I convert the actual selections to a list and feed into a validator that is shared with other collection components. Also, not a big deal. The problem appears to be that the tree is not `EditableValueHolder` so it does not store its _submitted_ value for re-render on validation failure – Lucas Jan 18 '13 at 15:50
  • Okay. That makes sense. Is the `update="@this"` in those tree ajax event listeners really necessary? – BalusC Jan 18 '13 at 15:56
  • Clever hack, but I would need some way of indicating that the tree is in error, so I could add an oncomplete handler to the select/unselect that could check for failure state on the hidden input and update the tree. Also, if someone were to attempt a save that failed validation (submit whole form, render whole form) then the state of the tree would get dropped back to its incorrect model state. – Lucas Jan 18 '13 at 16:19
  • Bind `` to view like `binding="#{hidden}"` and use `#{hidden.valid}` elsewhere to check if validation failed or not. As to render of whole form, consider moving render to action method by `RequestContext#update()`. Yes, it gets uglier and uglier... :/ ;) – BalusC Jan 18 '13 at 16:52
  • I don't follow this _As to render of whole form, consider moving render to action method by `RequestContext#update()`_. Can you elaborate? – Lucas Jan 18 '13 at 17:08
  • I mean, instead of `update="@form"` in view, use `RequestContext.getCurrentInstance().update(form.getClientId())` in action method. This way form is only updated when action method is hit. You can of course also skip the `` from update. See also e.g. http://stackoverflow.com/questions/12614882/how-to-exclude-child-component-in-ajax-update-of-a-parent-component/12619539#12619539 – BalusC Jan 18 '13 at 17:09
  • you are right, ugly, but i will give it a try. if this doesn't work, i think i am gonna have to write my own tree component... perhaps as a primefaces-extension... – Lucas Jan 18 '13 at 17:11
  • Ok, the results of your latest suggestion: 1) move update to action method - chicken/egg, never shows validation errors unless validation succeed. 2) use primefaces not: after just clicking back and forth a few times, the not eventually stops working, and i have no clue why... I think it may be time to give up on `` – Lucas Jan 18 '13 at 17:41
  • 1) This is expected. You just have to use `autoUpdate="true"` on `p:message(s)/growl`. Sorry, forgot to mention that a bit more explicitly. 2) Doesn't sound good. – BalusC Jan 18 '13 at 17:58
  • 1) That would update the failure message, but not the style on the component itself (adds ui-state-error class to throw red borders around the input). – Lucas Jan 18 '13 at 18:29
2

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 actionListeners 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...

Lucas
  • 14,227
  • 9
  • 74
  • 124