I am trying to get a bean injected into a custom ConstraintValidator
. I have come across some things:
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).
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With