/**
*
*/
package org.richfaces.validator;
import java.beans.FeatureDescriptor;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.InvalidValue;
/**
* Perform validation by Hibernate Validator annotations
*
* @author asmirnov
*
*/
public class BeanValidator {
private static final String RESOURCE_BUNDLE_IS_NOT_REGISTERED_FOR_CURRENT_LOCALE = "Resource bundle is not registered for current locale";
private static final String FACES_CONTEXT_IS_NULL = "Faces context is null";
private static final String INPUT_PARAMETERS_IS_NOT_CORRECT = "Input parameters is not correct.";
private static final String LOCALE_IS_NOT_SET = "Locale is not set";
private static final String VIEW_ROOT_IS_NOT_INITIALIZED = "ViewRoot is not initialized";
public static final String VALIDATOR_PARAM = BeanValidator.class.getName();
private Map<ValidatorKey, ClassValidator<? extends Object>> classValidators = new ConcurrentHashMap<ValidatorKey, ClassValidator<? extends Object>>();
private BeanValidator() {
// This is a "singleton"-like class. Only factory methods allowed.
}
/**
* Create BeanValidator instance. For a Junit tests only.
*
* @return
*/
static BeanValidator createInstance() {
// TODO - get instance class name from a "META-INF/service"
// If the Seam framework is active, use org.jboss.seam.core.Validators
// component should be used.
return new BeanValidator();
}
private static final Object MUTEX = new Object();
/**
* Return BeanValidator object from a ServletContext attribute. Create new
* instance if none is defined.
*
* @param context
* @return
*/
public static BeanValidator getInstance(FacesContext context) {
ExternalContext externalContext = context.getExternalContext();
externalContext.getContext();
BeanValidator instance;
// TODO - use properly synchronization mutex ?
synchronized (MUTEX) {
Map<String, Object> applicationMap = externalContext
.getApplicationMap();
instance = (BeanValidator) applicationMap.get(VALIDATOR_PARAM);
if (null == instance) {
// Vaildator not initialized - create and store new instance.
instance = createInstance();
applicationMap.put(VALIDATOR_PARAM, instance);
}
}
return instance;
}
/**
* Perform Validation for a new value.
*
* @param context
* current faces context.
* @param target
* {@link ValueExpression} for a value assignment.
* @param value
* new value for validation
* @return null if no validation errors. Array of the validation messages
* otherwise.
* @throws FacesException
* if locale or context not properly initialized
*/
public String[] validate(FacesContext context, ValueExpression target,
Object value) {
// TODO - check null parameters.
if (null == context) {
throw new FacesException(INPUT_PARAMETERS_IS_NOT_CORRECT);
}
String[] validationMessages = null;
if (null != target) {
ELContext elContext = context.getELContext();
ValidationResolver validationResolver = new ValidationResolver(
elContext.getELResolver(), calculateLocale(context));
ELContextWrapper wrappedElContext = new ELContextWrapper(elContext,
validationResolver);
// TODO - handle ELExceptions ?
try {
target.setValue(wrappedElContext, value);
} catch (ELException e) {
throw new FacesException(e);
}
if (!validationResolver.isValid()) {
validationMessages = validationResolver.getValidationMessages();
}
}
return validationMessages;
}
protected Locale calculateLocale(FacesContext context) {
if (null == context.getViewRoot()) {
throw new FacesException(VIEW_ROOT_IS_NOT_INITIALIZED);
} else if (null == context.getViewRoot().getLocale()) {
throw new FacesException(LOCALE_IS_NOT_SET);
}
Locale locale = context.getViewRoot().getLocale();
return locale;
}
// Method for checking input parameters for prevent NPE
private void checkInputParameters(FacesContext context,
ValueExpression target) {
if (null == context || null == target ) {
throw new FacesException(INPUT_PARAMETERS_IS_NOT_CORRECT);
}
}
/**
* Validate bean property for a new value. TODO - localization ?
*
* @param base
* - bean
* @param property
* - bean property name.
* @param value
* new value.
* @return null for a valid value, array of the validation messages
* othervise.
*/
public String[] validate(Object base, String property, Object value,
Locale locale) {
InvalidValue[] invalidValues = validateBean(base, property, value,
locale);
if (null == invalidValues) {
return null;
} else {
String[] result = new String[invalidValues.length];
for (int i = 0; i < invalidValues.length; i++) {
InvalidValue invalidValue = invalidValues[i];
result[i] = invalidValue.getMessage();
}
return result;
}
}
@SuppressWarnings("unchecked")
public String[] validateGraph(FacesContext context, Object value,
Set<String> profiles) {
if (null == context) {
throw new FacesException(INPUT_PARAMETERS_IS_NOT_CORRECT);
}
String validationMessages[] = null;
if (null != value) {
ClassValidator<Object> validator = (ClassValidator<Object>) getValidator(
value.getClass(), calculateLocale(context));
if (validator.hasValidationRules()) {
InvalidValue[] invalidValues = validator
.getInvalidValues(value);
if (null != invalidValues && invalidValues.length > 0) {
validationMessages = new String[invalidValues.length];
for (int i = 0; i < invalidValues.length; i++) {
InvalidValue invalidValue = invalidValues[i];
validationMessages[i] = invalidValue.getMessage();
}
}
}
}
return validationMessages;
}
/**
* Validate bean property of the base object aganist new value
*
* @param base
* @param property
* @param value
* @return
*/
protected InvalidValue[] validateBean(Object base, String property,
Object value, Locale locale) {
Class<? extends Object> beanClass = base.getClass();
InvalidValue[] invalidValues = validateClass(beanClass, property,
value, locale);
return invalidValues;
}
/**
* Validate bean property in the base class aganist new value.
*
* @param beanClass
* @param property
* @param value
* @return
*/
protected InvalidValue[] validateClass(Class<? extends Object> beanClass,
String property, Object value, Locale locale) {
ClassValidator<? extends Object> classValidator = getValidator(
beanClass, locale);
InvalidValue[] invalidValues = classValidator
.getPotentialInvalidValues(property, value);
return invalidValues;
}
/**
* Get ( or create ) {@link ClassValidator} for a given bean class.
*
* @param beanClass
* @return
*/
@SuppressWarnings("unchecked")
protected ClassValidator<? extends Object> getValidator(
Class<? extends Object> beanClass, Locale locale) {
// TODO - localization support.
ValidatorKey key = new ValidatorKey(beanClass, locale);
ClassValidator result = classValidators.get(key);
if (null == result) {
result = createValidator(beanClass, locale);
classValidators.put(key, result);
}
return result;
}
/*
* This method determine ResourceBundle, used in current request @param
* locale - user locale @return ResourceBundle instance
*/
private ResourceBundle getCurrentResourceBundle(Locale locale) {
if (null == FacesContext.getCurrentInstance()
|| null == FacesContext.getCurrentInstance().getApplication()) {
throw new FacesException(FACES_CONTEXT_IS_NULL);
}
String appBundle = FacesContext.getCurrentInstance().getApplication()
.getMessageBundle();
if (null == appBundle || null == locale) {
return null;
}
ResourceBundle bundle = ResourceBundle.getBundle(appBundle, locale);
return bundle;
}
/*
* Method for create new instance of ClassValidator, if same not in cache.
*
* @param beanClass - Class to validate @param locale - user Locale, used
* during validation process @return ClassValidator instance
*/
@SuppressWarnings("unchecked")
private ClassValidator<? extends Object> createValidator(
Class<? extends Object> beanClass, Locale locale) {
ResourceBundle bundle = getCurrentResourceBundle(locale);
return bundle == null ? new ClassValidator(beanClass)
: new ClassValidator(beanClass, bundle);
}
/**
* Wrapper class for a {@link ELResolver}. For a setValue method, perform
* validation instead of real assignment.
*
* @author asmirnov
*
*/
final class ValidationResolver extends ELResolver {
/**
* Original resolver.
*/
private final ELResolver parent;
private boolean valid = true;
private String[] validationMessages = null;
private Locale locale = null;
/**
* @param parent
*/
public ValidationResolver(ELResolver parent, Locale locale) {
this.parent = parent;
this.locale = locale;
}
public boolean isValid() {
// TODO Auto-generated method stub
return valid;
}
/**
* @param context
* @param base
* @return
* @see javax.el.ELResolver#getCommonPropertyType(javax.el.ELContext,
* java.lang.Object)
*/
public Class<?> getCommonPropertyType(ELContext context, Object base) {
return parent.getCommonPropertyType(context, base);
}
/**
* @param context
* @param base
* @return
* @see javax.el.ELResolver#getFeatureDescriptors(javax.el.ELContext,
* java.lang.Object)
*/
public Iterator<FeatureDescriptor> getFeatureDescriptors(
ELContext context, Object base) {
return parent.getFeatureDescriptors(context, base);
}
/**
* @param context
* @param base
* @param property
* @return
* @see javax.el.ELResolver#getType(javax.el.ELContext,
* java.lang.Object, java.lang.Object)
*/
public Class<?> getType(ELContext context, Object base, Object property) {
return parent.getType(context, base, property);
}
/**
* @param context
* @param base
* @param property
* @return
* @see javax.el.ELResolver#getValue(javax.el.ELContext,
* java.lang.Object, java.lang.Object)
*/
public Object getValue(ELContext context, Object base, Object property) {
return parent.getValue(context, base, property);
}
/**
* @param context
* @param base
* @param property
* @return
* @see javax.el.ELResolver#isReadOnly(javax.el.ELContext,
* java.lang.Object, java.lang.Object)
*/
public boolean isReadOnly(ELContext context, Object base,
Object property) {
return parent.isReadOnly(context, base, property);
}
/**
* @param context
* @param base
* @param property
* @param value
* @see javax.el.ELResolver#setValue(javax.el.ELContext,
* java.lang.Object, java.lang.Object, java.lang.Object)
*/
public void setValue(ELContext context, Object base, Object property,
Object value) {
if (null != base && null != property) {
context.setPropertyResolved(true);
//https://jira.jboss.org/jira/browse/RF-4034
//apache el looses locale information during value resolution,
//so we use our own
validationMessages = validate(base, property.toString(), value, locale);
valid = null == validationMessages
|| 0 == validationMessages.length;
}
}
/**
* @return the validationMessages
*/
public String[] getValidationMessages() {
return validationMessages;
}
}
/**
* Class for identify validator instance by locale
*
* @author amarkhel
*
*/
static class ValidatorKey {
private final Class<? extends Object> validatableClass;
private final Locale locale;
/**
* Constructor for ValidatorKey object
*
* @param validatableClass
* - class to validate
* @param locale
* - User locale to determine Resource bundle, used during
* validation process
*/
public ValidatorKey(Class<? extends Object> validatableClass,
Locale locale) {
this.validatableClass = validatableClass;
this.locale = locale;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((locale == null) ? 0 : locale.hashCode());
result = prime
* result
+ ((validatableClass == null) ? 0 : validatableClass
.hashCode());
return result;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof ValidatorKey))
return false;
ValidatorKey other = (ValidatorKey) obj;
if (locale == null) {
if (other.locale != null)
return false;
} else if (!locale.equals(other.locale))
return false;
if (validatableClass == null) {
if (other.validatableClass != null)
return false;
} else if (!validatableClass.equals(other.validatableClass))
return false;
return true;
}
}
}