custom validation message works for some errors - java

I have this web application where I have setup bean validation and it works fantastically well. But for one field, it doesn't read the message that I have setup in my validation.properties file. For other fields, it works. I am perplexed as to why this happens? Let me show you my setup:
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setCacheSeconds(0);
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
messageSource.setBasenames("/WEB-INF/Validation/validation"); //I have tried setBaseName as well but same difference.
return messageSource;
}
#Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(this.messageSource());
return validator;
}
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(this.localValidatorFactoryBean());
return processor;
}
#Autowired
SpringValidatorAdapter validator; //global validator
#Override
public Validator getValidator() {
return this.validator;
}
My properties file is in correct location. I know that because for some errors, messages are read from this validation.properties file. Location is:
src\main\webapp\WEB-INF\Validation\validation.properties
Content inside it is:
validate.user.yearsexperience = Only numbers can be used here.
validate.user.phone = Phone numbers can only be digits and should exactly be 10 digits.
validate.user.name.blank = Name cannot be left empty or Cannot contain more than 30 chars. It will accept letters only. No numbers or symbols are allowed either.
Then I setup following error codes in anticipation of bean validator catching one of these messages (but it did not).
typeMismatch.user.yearsexperience = Only numbers can be used here.
typeMismatch.yearsexperience = Only numbers can be used here.
typeMismatch.java.lang.Integer = Only numbers can be used here.
typeMismatch = Only numbers can be used here.
So when I enter wrong data for phone or name field, JSP shows those messages that I have set in my validation.properties file, but for yearsexperience field, it shows its own typeMismatch or cannot convert String to Integer, etc exceptions.
Here let me show you what Annotations I used over my model:
public class User{
#NotBlankNoFancyLettersOrNumbers(message = "{validate.user.name.blank}") //My own custom annotation. Works like a charm and when user enters wrong data, it shows my custom error.
private String name;
#Pattern(regexp="(^$|[0-9]{10})", message = "{validate.user.phone}") //javax's builtin validation constraint
private String phone;
#So many things I have tried. But first I started with #Pattern
private Integer yearsexperience;
For this Integer field, I cannot use #Pattern because it can only be put over String fields (am I correct here?).
Then I tried #Digits. When a user enters wrong input, it sure works, but doesn't show my error message, it shows a typeMismatch exception to the user. Then I made my own custom annotation and I gave it a name #DigitsOnly. Same issue, it would't show my error message. It shows the same old typeMismatch exception.
At this point, I thought maybe there is a problem with form binding (I should not have thought that because bean validation works properly for other fields). Anyway, I registered my own custom editor like this:
#InitBinder("user")
protected void binder(WebDataBinder binder, HttpSession session) throws Exception {
binder.registerCustomEditor(Date.class, new DateConvertor()); //This is unrelated to my question but it works!
binder.registerCustomEditor(Integer.class, "yearsexperience", new IntegerConvertor(session.getAttribute("user"), this.userdao));
}
This is my class that extends PropertyEditorSupport:
public class IntegerConvertor extends PropertyEditorSupport {
private User user;
private UserDAO userdao;
public IntegerConvertor(){}
public IntegerConvertor(Object object, UserDAO userdao) {
if (object instanceof User){
this.user= new User((User) object);
}
this.userdao = userdao;
}
#Override
public void setAsText(String value) {
if (!value.matches("[0-9]+")) { //if incoming value DOES NOT contains numbers only
setValue(this.userdao.getYearsExperienceBeforeChange(this.user.getId()));
} else {
setValue(value);
}
}
Now, I did this in a hope that typeMismatch exception will not be shown to the user, but still it shows that exception. (I knew that from this PropertyEditorSupport, or it wouldn't read custom error message from validation.properties file).
Anyway, then I tried to add a custom validator thru WebDataBinder.addValidator(myCustomValidator) like this:
#InitBinder("user")
protected void binder(WebDataBinder binder, HttpSession session) throws Exception {
binder.registerCustomEditor(Date.class, new DateConvertor());
binder.addValidators(new myCustormValidator());
}
This is how my custom validator looks,
public class MyCustormValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return User.class.equals(clazz); //return User.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
User user = (User) target;
String a = Integer.toString(user.getYearsExperience());
if(!a.matches("[0-9]+")){
errors.rejectValue("yearsexperience","validate.user.yearsexperience");
}
}
}
I converted that Integer to String because then I was able to check if the incoming value contains digits only and not alphabets. But here it causes another issue: it doesn't show the incoming value, it shows the old value. And because old value was already correct, it doesn't raise any issue. So I added else statement, just to throw an error in a hope that now at least it will show my custom error. But it does not!
Therefore, I have come here for help. Any help is greatly appreciated.
TL;DR Why custom error message doesn't show up for one field. But for other fields it works perfectly fine.

Why the custom error doesn't show up for some fields but for others it works perfectly fine
Because there are two different components are involved:
hibernate-validator that is using for bean validation
spring-mvc that is binding user values from a to a POJO class
You've properly configured message source for bean validation and it works.
But then you're customizing typeMismatch message and looks like Spring MVC doesn't see this. Typically it's enough to define bean with name messageSource but for some strange reason in your case you need something else.
My guess is that you have 2 contexts and messageSource is defined in the context that isn't visible to Spring MVC.
UPDATE:
I looked into example project on GitHub (http://github.com/farazdurrani/sscce) and I see that the problem with non-working typeMismatch message not because of the configuration. It happens because you're not using standard tags and try to show error messages manually (by invoking getDefaultMessage() method on each error from Errors.getAllErrors()). If you try to use <form:errors path="*"/>, you'll see that all error messages have been properly localized.

Related

Unable to resolve variable from properties file when tried to access as function parameter using #Value annotation

This may be silly question to ask but i'm unable to find any satisfactory solution to my problem. In java we don't have the concept of default variables so i am trying to give default value from properties file to my function parameters/arguments using #Value annotation, but i'm always getting null and i'm unable to figure why is this happening. Please help me to solve the issue or provide me some appropriate link/reference which may solve my issue.
MainApplication.java
#SpringBootApplication
public class Application
{
public static void main(String[] args)
{
ApplicationContext context = SpringApplication.run(NetappApplication.class, args);
Sample sample = context.getBean(Sample.class);
System.out.println(sample.check(null));
}
}
Sample.java
public interface Sample
{
public String check(String message);
}
SampleImpl.java
#Service
#PropertySource("classpath:app.properties")
public class SampleImpl implements Sample
{
#Value("${test}")
String message1;
#Override
public String check(#Value("${test}") String message)
{
return message;
}
}
app.properties
test=anand
But you are passing null to your method...
Perhaps what you want to do is to assign default value to test in case it's not defined in property file:
#Value("${test:default}");
Then, when properties are autowired by Spring if placeholder resolver doesn't get the value from props file, it will use what is after :.
The best use case for this (that I can think of) is when you create Spring configuration.
Let's say you have a configuration class: for DB access. Simply put:
#Configuration
public class DbConfig {
#Value("${url:localhost}")
String dbUrl;
// rest for driver, user, pass etc
public DataSource createDatasource() {
// here you use some DataSourceBuilder to configure connection
}
}
Now, when Spring application starts up, properties' values are resolved, and as I wrote above you can switch between value from property and a default value. But it is done once, when app starts and Spring creates your beans.
If you want to check incoming argument on runtime, simple null check will be enough.
#Value("${test}")
String message1;
#Override
public String check(String message) {
if (message == null) {
return message1;
}
}

Spring Managed Custom Validator not being used from endpoint

I've been at this for a while, but I have a Spring managed custom validator that looks like the below, I have some print statements in there which I'll get to later
#Component
public class BulkUpdateValidator implements ConstraintValidator<ValidBulkUpdate, BulkUpdate> {
#Autowired
ObjectMapper mapper;
public BulkUpdateValidator(){
System.out.println(this.toString());
}
#PostConstruct
public void post(){
System.out.println(mapper);
System.out.println(this.toString());
}
public boolean isValid(BulkUpdate update, ConstraintValidatorContext context){
System.out.println(this.toString());
System.out.println(mapper);
}
... other validator methods ...
}
My controller method: (NOTE: my controller class is annotated with #Validated at the top)
#RequestMapping(...)
public #ResponseBody RestResponse bulkUpdate(#Valid #ValidBulkUpdate Bulkupdate bulkUpdate){
... stuff here ...
}
My Bean:
public class BulkUpdate {
#NotEmpty
public List<String> recordIds;
#NotEmpty
#Valid
public List<FieldUpdate> updates;
.... getters and setters ....
}
Here's my problem, when I execute the endpoint it get a NullPointerException when I attempt to use the autowired mapper. The output from the print statements I posted above are quite telling. In both the constructor and the #PostConstruct sections I get the same Object ID for the validator and I also get an ID for the mapper. However, once isValid is called, it prints out a different Object ID. I know the spring managed validator is being created, but it's not being used.
Furthermore, I've tried to remove the #ValidBulkUpdate annotation from the REST endpoint and put it inside a wrapper object, thinking that maybe #Valid was necessary to get spring to take over, like below:
public #ResponseBody RestResponse bulkUpdate(#Valid BulkupdateWrapper bulkUpdate){
... stuff here ...
}
And wrapper
public class BulkUpdateWrapper {
#ValidBulkUpdate
private BulkUpdate update;
.... getter and setter ....
}
This leaves me with a whole new error which is even weirder:
"JSR-303 validated property 'update.org.hibernate.validator.internal.engine.ConstraintViolationImpl' does not have a corresponding accessor"
I'm not sure where to turn, hopefully someone has an idea. Either how to get it to use the Spring managed validator, or how to remove that vague error when I use the object wrapper;
What's worse, is I have MockMvc based Integration tests for this that run flawlessly, this only happens when I deploy it.
UPDATE
So I kept my wrapper and changed #Valid to #Validated and now my error is the following: "NotReadablePropertyException: Bean property 'update.field' does not have a corresponding accessor for Spring data binding"
Fun fact, there is no property called "field"

Override DropWizard ConstraintViolation message

So I want to change the validation messages used to validate a model through a DropWizard resource.
I'm using java bean validation annotations. For example here is one of the fields I want to validate:
#NotEmpty(message = "Password must not be empty.")
I can test this works as expected using a validator.
However when I use DropWizard to do the validation on the resource it adds some extra stuff to that message. What I see is this - password Password must not be empty. (was null) and I've found the code that does this here - https://github.com/dropwizard/dropwizard/blob/master/dropwizard-validation/src/main/java/io/dropwizard/validation/ConstraintViolations.java
Specifically this method -
public static <T> String format(ConstraintViolation<T> v) {
if (v.getConstraintDescriptor().getAnnotation() instanceof ValidationMethod) {
final ImmutableList<Path.Node> nodes = ImmutableList.copyOf(v.getPropertyPath());
final ImmutableList<Path.Node> usefulNodes = nodes.subList(0, nodes.size() - 1);
final String msg = v.getMessage().startsWith(".") ? "%s%s" : "%s %s";
return String.format(msg,
Joiner.on('.').join(usefulNodes),
v.getMessage()).trim();
} else {
return String.format("%s %s (was %s)",
v.getPropertyPath(),
v.getMessage(),
v.getInvalidValue());
}
}
Is there any way I can override this behaviour? I just want to display the message that I set in the annotation...
Here is a programmatic solution in dropwizard 0.8:
public void run(final MyConfiguration config, final Environment env) {
AbstractServerFactory sf = (AbstractServerFactory) config.getServerFactory();
// disable all default exception mappers
sf.setRegisterDefaultExceptionMappers(false);
// register your own ConstraintViolationException mapper
env.jersey().register(MyConstraintViolationExceptionMapper.class)
// restore other default exception mappers
env.jersey().register(new LoggingExceptionMapper<Throwable>() {});
env.jersey().register(new JsonProcessingExceptionMapper());
env.jersey().register(new EarlyEofExceptionMapper());
}
I think it's more reliable than a config file. And as you can see it also enables back all other default exception mappers.
ConstraintViolationExceptionMapper is the one which uses that method. In order to override it, you need to deregister it and register your own ExceptionMapper.
Remove the exception mapper(s)
Dropwizard 0.8
Add the following to your yaml file. Note that it will remove all the default exception mappers that dropwizard adds.
server:
registerDefaultExceptionMappers: false
Dropwizard 0.7.x
environment.jersey().getResourceConfig().getSingletons().removeIf(singleton -> singleton instanceof ConstraintViolationExceptionMapper);
Create and add your own exception mapper
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException exception) {
// get the violation errors and return the response you want.
}
}
and add your exception mapper in your application class.
public void run(T configuration, Environment environment) throws Exception {
environment.jersey().register(ConstraintViolationExceptionMapper.class);
}
#ValidationMethod should be useful here. isn't it?
http://www.dropwizard.io/0.9.0/docs/manual/validation.html
#ValidationMethod(message="Password cannot be empty")
#JsonIgnore
public boolean isPasswordProvided() {
return false if password not provided;
}

Spring Hibernate Validator check step by step

I had many constraint on a single property, like this:
#NotEmpty
#Size(min = 2, max = 20)
#Pattern(regexp= "^[0-9a-z_A-Z\u4e00-\u9fa5]+$")
private String username;
but, when it works, it will check all constraints, and I just want check step by step, so how can i do? and I found a special constraint, that is #Email constraint, I do like this:
#NotEmpty
#Email
private String email;
I found it will check step by step, if the #NotEmpty constraint check failed, it will not check #Email constraint, I just found #Email have the function, I want to say, there is some especial for #Email?
It is so confused for me, and I hoped someone could help me, thanks.
Sounds like you should look into creating a custom validator.
here is a good example on how to setup a basic custom validator (go to section called Custom Validator Implementations, especially the way they do the EmployeeFormValidator):
http://www.journaldev.com/2668/spring-mvc-form-validation-example-using-annotation-and-custom-validator-implementation
Creata a custom ordering in there that you want to have and then just bind it within your controller against your expected object (or call the validate function.
If you are gonna use JSR-303, that is what you are doing in your code and custom validator, it's not easy for you control the order of validation. Therefore, better to transform all those validation in a custom validator, it's much more flexible.
(1)Implements the Spring validator interface
public XXXValidator implements Validator {
#Autowired MessageSource messageSource
public boolean supports(Class clazz) {
return XXXX.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
//do your validation here
if(....){
e.rejectValue(..,..,messageSource.getMessage(...),..,..)
}
}
}
(2) in you controller
XXXValidator validator=new XXXValidator ();
#RequestMapping(...,....)
public String handlePostMethodExample(#ModelAndAttribute XXX xxx,BindingResult error)
validator.validate({instance of XXX object here});
if(error.hasErrors(){
//handle error here
}
}

mongodb multi tenacy spel with #Document

This is related to
MongoDB and SpEL Expressions in #Document annotations
This is the way I am creating my mongo template
#Bean
public MongoDbFactory mongoDbFactory() throws UnknownHostException {
String dbname = getCustid();
return new SimpleMongoDbFactory(new MongoClient("localhost"), "mydb");
}
#Bean
MongoTemplate mongoTemplate() throws UnknownHostException {
MappingMongoConverter converter =
new MappingMongoConverter(mongoDbFactory(), new MongoMappingContext());
return new MongoTemplate(mongoDbFactory(), converter);
}
I have a tenant provider class
#Component("tenantProvider")
public class TenantProvider {
public String getTenantId() {
--custome Thread local logic for getting a name
}
}
And my domain class
#Document(collection = "#{#tenantProvider.getTenantId()}_device")
public class Device {
-- my fields here
}
As you see I have created my mongotemplate as specified in the post, but I still get the below error
Exception in thread "main" org.springframework.expression.spel.SpelEvaluationException: EL1057E:(pos 1): No bean resolver registered in the context to resolve access to bean 'tenantProvider'
What am I doing wrong?
Finally figured out why i was getting this issue.
When using Servlet 3 initialization make sure that you add the application context to the mongo context as follows
#Autowired
private ApplicationContext appContext;
public MongoDbFactory mongoDbFactory() throws UnknownHostException {
return new SimpleMongoDbFactory(new MongoClient("localhost"), "apollo-mongodb");
}
#Bean
MongoTemplate mongoTemplate() throws UnknownHostException {
final MongoDbFactory factory = mongoDbFactory();
final MongoMappingContext mongoMappingContext = new MongoMappingContext();
mongoMappingContext.setApplicationContext(appContext);
// Learned from web, prevents Spring from including the _class attribute
final MappingMongoConverter converter = new MappingMongoConverter(factory, mongoMappingContext);
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
return new MongoTemplate(factory, converter);
}
Check the autowiring of the context and also
mongoMappingContext.setApplicationContext(appContext);
With these two lines i was able to get the component wired correctly to use it in multi tenant mode
The above answers just worked partially in my case.
I've been struggling with the same problem and finally realized that under some runtime execution path (when RepositoryFactorySupport relies on AbstractMongoQuery to query MongoDB, instead of SimpleMongoRepository which as far as I know is used in "out of the box" methods provided by SpringData) the metadata object of type MongoEntityMetadata that belongs to MongoQueryMethod used in AbstractMongoQuery is updated only once, in a method named getEntityInformation()
Because MongoQueryMethod object that holds a reference to this 'stateful' bean seems to be pooled/cached by infrastructure code #Document annotations with Spel not always work.
As far as I know as a developer we just have one choice, use MongoOperations directly from your #Repository bean in order to be able to specify the right collection name evaluated at runtime with Spel.
I've tried to use AOP in order to modify this behaviour, by setting a null collection name in MongoEntityMetadata but this does not help because changes in AbstractMongoQuery inner classes, that implement Execution interface, would also need to be done in order to check if MongoEntityMetadata collection name is null and therefore use a different MongoTemplate method signature.
MongoTemplate is smart enough to guess the right collection name by using its private method
private <T> String determineEntityCollectionName(T obj)
I've a created a ticket in spring's jira https://jira.spring.io/browse/DATAMONGO-1043
If you have the mongoTemplate configured as in the related issue, the only thing i can think of is this:
<context:component-scan base-package="com.tenantprovider.package" />
Or if you want to use annotations:
#ComponentScan(basePackages = "com.tenantprovider.package")
You might not be scanning the tenant provider package.
Ex:
#ComponentScan(basePackages = "com.tenantprovider.package")
#Document(collection = "#{#tenantProvider.getTenantId()}_device")
public class Device {
-- my fields here
}

Categories