Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Spring's LocalValidatorFactoryBean with JSF

I am trying to get a bean injected into a custom ConstraintValidator. I have come across some things:

  • CDI is supported in validation-api-1.1.0 (Beta available)
  • Hibernate Validator 5 seems to implement validation-api-1.1.0 (Alpha available)
  • Use Seam validation module
  • Use Spring's LocalValidatorFactoryBean

The last one seems most appropriate for my situation since we're already using Spring (3.1.3.Release).

I have added the validator factory to the XML application context and annotations are enabled:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:component-scan base-package="com.example" />
    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
</beans>

The validator:

public class UsernameUniqueValidator implements
    ConstraintValidator<Username, String>
{
    @Autowired
    private PersonManager personManager;

    @Override
    public void initialize(Username constraintAnnotation)
    {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context)
    {
        if (value == null) return true;
        return personManager.findByUsername(value.trim()) != null;
    }
}

The validation is applied to a Person:

public class Person
{
    @Username
    private String username;
}

And the backing bean:

@Named
@Scope("request")
public class PersonBean
{
    private Person person = new Person();
    @Inject
    private PersonManager personManager;

    public create()
    {
        personManager.create(person);
    }
}

And in the JSF page I have:

<p:inputText value="#{personBean.person.username}" />

The validator is invoked but the field is not autowired/injected and stays null. This of course trows a NullPointerException.

I am testing this with Hibernate validator 4.2 (since LocalValidatorFactoryBean should be able to do this I think).

like image 934
siebz0r Avatar asked Oct 05 '22 08:10

siebz0r


2 Answers

I also faced the same issue. In my case Spring+MyFaces+RichFaces are used. During the application startup Spring creates it's LocalValidatorFactoryBean, but MyFaces doesn't use that bean as a validation factory. Instead MyFaces and RichFaces both used their own validators even with spring-faces module.

To figure out how to make faces use LocalValidatorFactoryBean I looked inside javax.faces.validator.BeanValidator createValidatorFactory method. This method is used by MyFaces to create ValidatorFactory every time when validation is required. Inside of that method you can see the following:

Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
Object attr = applicationMap.get(VALIDATOR_FACTORY_KEY);
if (attr instanceof ValidatorFactory)
{
    return (ValidatorFactory) attr;
}
else
{
    synchronized (this)
    {
        if (_ExternalSpecifications.isBeanValidationAvailable())
        {
            ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
            applicationMap.put(VALIDATOR_FACTORY_KEY, factory);
            return factory;
        }
        else
        {
            throw new FacesException("Bean Validation is not present");
        }
    }
}

So as you can see it first tries to load ValidatorFactory from context before creating a new instance. So I implemented the following solution to make faces use Spring LocalValidatorFactoryBean: I created a SystemEventListener which runs on PostConstructApplicationEvent. This listener get's a Spring WebApplicationContext from servlet context, retrieves instance of LocalValidatorFactoryBean from it and stores it in ExternalContext ApplicationMap.

public class SpringBeanValidatorListener implements javax.faces.event.SystemEventListener {
    private static final long serialVersionUID = -1L;

    private final Logger logger = LoggerFactory.getLogger(SpringBeanValidatorListener.class);

    @Override
    public boolean isListenerForSource(Object source) {
        return true;
    }

    @Override
    public void processEvent(SystemEvent event) {
        if (event instanceof PostConstructApplicationEvent) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            onStart(facesContext);
        }
    }

    private void onStart(FacesContext facesContext) {
        logger.info("--- onStart ---");

        if (facesContext == null) {
            logger.warn("FacesContext is null. Skip further steps.");
            return;
        }

        ServletContext context = getServletContext(facesContext);

        if (context == null) {
            logger.warn("ServletContext is not available. Skip further steps.");
            return;
        }

        WebApplicationContext webApplicationContext = (WebApplicationContext) context.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

        if (webApplicationContext == null) {
            logger.warn("Spring WebApplicationContext was not set in ServletContext. Skip further steps.");
            return;
        }

        LocalValidatorFactoryBean validatorFactory = null;

        try {
            validatorFactory = webApplicationContext.getBean(LocalValidatorFactoryBean.class);
        } catch (BeansException ex){
            logger.warn("Cannot get LocalValidatorFactoryBean from spring context.", ex);
        }

        logger.info("LocalValidatorFactoryBean loaded from Spring context.");

        Map<String, Object> applicationMap = facesContext.getExternalContext().getApplicationMap();
        applicationMap.put(BeanValidator.VALIDATOR_FACTORY_KEY, validatorFactory);

        logger.info("LocalValidatorFactoryBean set to faces context.");
    }

    private ServletContext getServletContext(FacesContext facesContext) {
        return (ServletContext) facesContext.getExternalContext().getContext();
    }
}

So when MyFaces try to get ValidatorFactory for the first time, LocalValidatorFactoryBean is already there and MyFaces don't create a new instance.

like image 70
Aliaksei Avatar answered Oct 10 '22 03:10

Aliaksei


It is definately the way to go to add your own custom ValidatorFactory to the application map using the key BeanValidator.VALIDATOR_FACTORY_KEY. But instead of using a javax.faces.event.SystemEventListener, you could also approach it from the spring side. Registering your ValidatorFactory as an attribute in the ServletContext will be enough for it to be picked up and added to the application map (which is an abstraction for either the ServletContext or PortletContext, whatever you are using).

So the question is: how to add a spring bean as an attribute to the ServletContext. My solution was to use a helper bean that implements ServletContextAware:

public class ServletContextAttributePopulator implements ServletContextAware {

    Map<String,Object> attributes;

    public Map<String, Object> getAttributes() {
        return attributes;
    }

    public void setAttributes(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        for (Map.Entry<String,Object> entry : attributes.entrySet()) {
            servletContext.setAttribute(entry.getKey(), entry.getValue());
        }
    }

}

Note that you could use this class for any type of bean you want to add to the ServletContext.
In your spring context, you would then add:

<bean  id="servletContextPopulator" class="my.package.ServletContextAttributePopulator">
    <property name="attributes">
    <map>
        <entry key="javax.faces.validator.beanValidator.ValidatorFactory" value-ref="validator"/>
    </map>
    </property>
</bean>

where "validator" is the id of your LocalValidatorFactoryBean.

like image 45
Kristiaan Avatar answered Oct 10 '22 04:10

Kristiaan