Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSF 2.0 Required Field Label Decorator for properties with @NotNull Constraint

I have a JSF 2.0 application which also uses Primefaces 3.3. Currently there is a nice feature where a label is decorated with an asterisk if the related <p:inputText> uses a required="true" attribute.

This field is bound to a bean property which is annotated with @NotNull validation constraint. It seems redundant and error prone to have to also add required="true" in the XHTML when the bean property is already annotated with @NotNull.

Is there a hook or some way to automatically decorate labels for components that are bound to properties with @NotNull?

Any thoughts or suggestions are greatly appreciated.

like image 861
user2055510 Avatar asked Feb 08 '13 19:02

user2055510


2 Answers

Note: This is a hack. It may have performance implications due to it's use of introspection

  1. At a basic level, what you need to know if the field is annotated with @NotNull. Perform this check in a sensible place like @PostConstruct for a view scoped bean. Declare a global variable to determine the required attribute

    boolean  requiredAttribute;        
    
    @PostConstruct
    public void init{ 
    Field theField = this.getClass().getField("theField");
    NotNull theAnnotation = theField.getAnnotation(NotNull.class);
    if(theAnnotation != null){
       requiredAttribute = true;
       }
    }
    
  2. Bind the required attribute to the variable in the backing bean

    <p:inputText id="inputit" required="#{myBean.requiredAttribute}"/>
    
like image 60
kolossus Avatar answered Sep 22 '22 08:09

kolossus


This solution is based on PF 6.0, I don't remember if BeanValidationMetadataExtractor is available in previous versions. Anyway, creating a DIY extractor is a simple task.

I had a similar problem. In my specific case:

  • User should be informed that a certain field (read UIInput) is required
  • I don't want to repeat required="true" on the comp since it's already bound to a @NotNull/@NotBlank property/field
  • In my case, a label component may not be present (and I don't like asterisked-labels)

So, here is what I have done:

import java.util.Set;
import javax.el.ValueExpression;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PreRenderComponentEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import javax.validation.constraints.NotNull;
import javax.validation.metadata.ConstraintDescriptor;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.omnifaces.util.Faces;
import org.primefaces.context.RequestContext;
import org.primefaces.metadata.BeanValidationMetadataExtractor;


public class InputValidatorConstraintListener implements SystemEventListener
{
    @Override
    public boolean isListenerForSource(Object source)
    {
        return source instanceof UIInput;
    }

    @Override
    public void processEvent(SystemEvent event) throws AbortProcessingException
    {
        if(event instanceof PreRenderComponentEvent)
        {
            UIInput component = (UIInput) event.getSource();

            component.getPassThroughAttributes().computeIfAbsent("data-required", k ->
            {
                ValueExpression requiredExpression = component.getValueExpression("required");
                if(requiredExpression != null || !component.isRequired())
                {
                    FacesContext context = Faces.getContext();
                    ValueExpression valueExpression = component.getValueExpression("value");
                    RequestContext requestContext = RequestContext.getCurrentInstance();

                    try
                    {
                        Set<ConstraintDescriptor<?>> constraints = BeanValidationMetadataExtractor.extractAllConstraintDescriptors(context, requestContext, valueExpression);
                        if(constraints != null && !constraints.isEmpty())
                        {
                            return constraints.stream()
                                .map(ConstraintDescriptor::getAnnotation)
                                .anyMatch(x -> x instanceof NotNull || x instanceof NotBlank || x instanceof NotEmpty);
                        }
                    }
                    catch(Exception e)
                    {
                        return false;
                    }
                }

                return false;
            });
        }
    }
}

and declare it in faces-config.xml:

<?xml version="1.0" encoding="utf-8"?>
<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">

    <application>
        <system-event-listener>
            <system-event-listener-class>it.shape.core.jsf.listener.InputValidatorConstraintListener</system-event-listener-class>
            <system-event-class>javax.faces.event.PreRenderComponentEvent</system-event-class>
        </system-event-listener>
    </application>

</faces-config>

With this listener UIInputs are rendered with a data-required passthrough attribute:

<input 
    id="form:editPanelMain:j_idt253" 
    name="form:editPanelMain:j_idt253" 
    type="text" 
    value="Rack Assemply" 
    size="80" 
    data-required="true"    <============================ NOTE THIS!!
    data-widget="widget_form_editPanelMain_j_idt253" 
    class="ui-inputfield ui-inputtext ui-widget ui-state-default ui-corner-all" 
    role="textbox" 
    aria-disabled="false" 
    aria-readonly="false">

Now, I use a css rule to highlight these fields:

input[data-required='true'], 
.ui-inputfield[data-required='true'], 
*[data-required='true']  .ui-inputfield {
    box-shadow: inset 0px 2px 2px #bf8f8f;
}

You can adapt this listener to either set the component as required or use another approach that suits your specific needs.

Another approach could be:

  • listen for UILabels instead of UIInputs
  • get the UIInput associated with the for/forValue ValueExpression of the label
  • check the UIInput for validation constraint
  • eventually invoke UIInput.setRequired(true)

The performance impact is negligible, as I have tested complex pages with ~3000 components.

like image 38
Michele Mariotti Avatar answered Sep 26 '22 08:09

Michele Mariotti