New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HV-1529 Add dynamic payload to HibernateConstraintValidator #890
Conversation
|
||
public ValidatorImpl withDynamicConstraintValidatorInitializationPayload(Object dynamicPayload) { | ||
this.constraintValidatorInitializationContext = new HibernateConstraintValidatorInitializationContextImpl( | ||
this.scriptEvaluatorFactory, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of saving a reference to the ScriptEvaluatorFactory
here inside the ValidatorImpl
class we could also add a getScriptEvaluatorFactory()
to the (Hibernate)ConstraintValidatorInitializationContext(Impl)
- if it would be ok to expose that ScriptEvaluatorFactory
from that context.
We could then replace this line here with this.constraintValidatorInitializationContext.getScriptEvaluatorFactory()
instead. I leave it up to you which approach to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We sure don't want to expose it (see the clear()
method for instance).
What you could do instead is creating another class that wraps the original HibernateConstraintValidatorInitializationContextImpl
and adds the payload. The original HibernateConstraintValidatorInitializationContextImpl
would simply return null for the payload.
-> but this comment won't be of any use to you if we move the payload to ValidatorFactory
/HibernateValidatorContext
level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mkurz Interesting idea. Happy to see that our vision covers more use cases than anticipated.
I started to add a few minor comments inline but then discovered the big concurrency issue.
Most of the comments are still valid even if you adopt the new approach I presented (not sure we should call the payload "dynamic" then as it won't be very dynamic).
|
||
/** | ||
* @param type The type of payload to retrieve | ||
* @return an instance of the specified type set by the user via |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make the long version the description of the method and keep a short one for the @return
.
* @param type The type of payload to retrieve | ||
* @return an instance of the specified type set by the user via | ||
* {@link ValidatorImpl#withDynamicConstraintValidatorInitializationPayload(Object)} or {@code null} if no constraint payload | ||
* if the given type has been set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/if the given type/of the given type/
/** | ||
* @param type The type of payload to retrieve | ||
* @return an instance of the specified type set by the user via | ||
* {@link ValidatorImpl#withDynamicConstraintValidatorInitializationPayload(Object)} or {@code null} if no constraint payload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/constraint payload/constraint validator payload/
|
||
public ValidatorImpl withDynamicConstraintValidatorInitializationPayload(Object dynamicPayload) { | ||
this.constraintValidatorInitializationContext = new HibernateConstraintValidatorInitializationContextImpl( | ||
this.scriptEvaluatorFactory, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We sure don't want to expose it (see the clear()
method for instance).
What you could do instead is creating another class that wraps the original HibernateConstraintValidatorInitializationContextImpl
and adds the payload. The original HibernateConstraintValidatorInitializationContextImpl
would simply return null for the payload.
-> but this comment won't be of any use to you if we move the payload to ValidatorFactory
/HibernateValidatorContext
level.
this.constraintValidatorInitializationContext = new HibernateConstraintValidatorInitializationContextImpl( validatorScopedContext.getScriptEvaluatorFactory(), validatorScopedContext.getClockProvider(), validatorScopedContext.getTemporalValidationTolerance(), null ); | ||
} | ||
|
||
public ValidatorImpl withDynamicConstraintValidatorInitializationPayload(Object dynamicPayload) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would stick to a more simple name. Something like withConstraintValidatorDynamicPayload()
. The fact that it is related to the initialization can be removed I think. We don't really care (mostly because it can still be used at runtime by adding an attribute to the constraint validator).
/** | ||
* The constraint initialization context is stored at this level to prevent creating a new instance each time we | ||
* initialize a new constraint validator as, for now, it only contains Validator scoped objects. | ||
*/ | ||
private final HibernateConstraintValidatorInitializationContext constraintValidatorInitializationContext; | ||
private HibernateConstraintValidatorInitializationContext constraintValidatorInitializationContext; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A wise man said to me one day: "Every time you remove a final
, God kills a kitten." (yes, he's so wise he uses backticks when he speaks).
You're paving the way to concurrency issues. Keep in mind that the ValidatorImpl
is shared across thread.
} | ||
|
||
public ValidatorImpl withDynamicConstraintValidatorInitializationPayload(Object dynamicPayload) { | ||
this.constraintValidatorInitializationContext = new HibernateConstraintValidatorInitializationContextImpl( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concurrency issue. You're not overriding the context for your validate()
call here but also for all the other random threads running. And the other threads will override it too.
I think we won't be able to make your approach work.
My opinion is that you'd be better served by having the payload at the ValidatorFactory
level AND the HibernateValidatorContext
level.
The drawback is that before your calls to validate()
you would have to instantiate the Validator
first by unwrapping the context and setting the payload. The Validator
creation is quite lightweight so I don't think it will be an issue in practice.
Could you try this approach and see how it goes with your use case?
Hi @mkurz, a couple of comments/questions:
Note there are two existing ways for passing in contextual information:
|
Matthias gave an example in the JIRA issue: "think of zip codes which differ for each country".
That's not what the PR does (and it indeed introduces concurrency issues I mentioned in my review). Frankly, taking a look at the Do you think it would be better to add a parameter to the various
Yup, I didn't mention it because we can't introduce the feature here anyway (concurrency issues). |
Btw, I think it makes sense to consider it If the same user makes several |
Ok, so the requirement is then: public class Address {
@ValidZipCode // acts differently based on the country
String zipCode;
Country country; ? As said, I think the easiest would be to use a class-level constraint or context injection into the validator then.
Actually, the problem with either approach is that the user typically isn't in the business of obtaining a validator (in the context of one request/transaction etc.) nor of calling Instead this will be done by some framework for them. I.e. usually passing in such context will not be available to them, whereas the other two approaches allow to get hold of contextual information also in such managed environment. In a way it's related to BVAL-237, too, i.e. the idea of making the root or leaf bean (or at least their properties in a detyped manner) available to constraint validators. |
Btw. there already is a way for passing |
Yeah, that's exactly what I proposed in my review :). |
@gunnarmorling I thought about it a bit more and I don't think having the ability to pass some context to constraint validators is useless. It might be handy to manage differences between 2 environments or 2 different apps using the same jars. Having them at VF and context level would be enough. @mkurz could you give us more details about your use case? As you might have noticed, @gunnarmorling is a bit skeptic about the usefulness of such addition so having more details on why it's not practical for you to use class level constraints and some data in the model would help. Thanks! |
@gsmet and @gunnarmorling thank you both very much for your comprehensive replies. I really appreciate that. So what is my use case? However in near future that That is how hibernate-validator is set up in Play righ now Actually at first I was thinking the same like what @gunnarmorling proposed - to add a parameter to all the various (unwrapped) About the suggestions @gunnarmorling mentioned
Status quo of this pull request However I just set up another branch which - like my original idea - adds a What do you think about all of this? Am I missing something? |
@@ -183,7 +183,8 @@ public ValidatorFactoryImpl(ConfigurationState configurationState) { | |||
getTemporalValidationTolerance( configurationState, properties ), | |||
getScriptEvaluatorFactory( configurationState, properties, externalClassLoader ), | |||
getFailFast( hibernateSpecificConfig, properties ), | |||
getTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ) | |||
getTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ), | |||
null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about just passing null
here. Suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what's you're missing here is a way to define it globally at the ValidatorFactory
level. That's why you're forced to pass null. But the API building the VF programmatically should allow to pass a global payload.
I don't understand what is the value of that. I think it's better to pass the You could either have a |
In case we don't pass a locale as dynamic payload but e.g. the whole play request context which could contain different data on each request we can't cache that validator like we could when just passing the locale as payload. So when no caching is possible, "copying" a validator and passing the payload is cheaper than using |
@mkurz just to let you know that I'm thinking about this. Will get back to you shortly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, after thinking a bit more about it, I think what was bugging me is that I'm not comfortable with creating Validator
s outside of the ValidatorFactory
. Thus I would really like us to stick to the approach you have here.
I think we can reduce the overhead of creating a brand new Validator
a bit by making the ValidationOrderGenerator
global to the ValidatorFactory
(this should be made in a separate commit).
We would still end up with a higher cost (basically, creating the cache key + the CHM lookup) than with your Validator
cloning idea but I think it's safer for the future.
I also added a couple of inline comments to improve the naming of the API.
I'd like to include it in our next minor if you can come up with an updated patch.
@@ -183,7 +183,8 @@ public ValidatorFactoryImpl(ConfigurationState configurationState) { | |||
getTemporalValidationTolerance( configurationState, properties ), | |||
getScriptEvaluatorFactory( configurationState, properties, externalClassLoader ), | |||
getFailFast( hibernateSpecificConfig, properties ), | |||
getTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ) | |||
getTraversableResolverResultCacheEnabled( hibernateSpecificConfig, properties ), | |||
null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what's you're missing here is a way to define it globally at the ValidatorFactory
level. That's why you're forced to pass null. But the API building the VF programmatically should allow to pass a global payload.
@@ -137,6 +137,12 @@ public HibernateValidatorContext temporalValidationTolerance(Duration temporalVa | |||
return this; | |||
} | |||
|
|||
@Override | |||
public HibernateValidatorContext dynamicPayload(Object dynamicPayload) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call it withConstraintValidatorPayload()
.
We use withDynamicPayload()
in other HV APIs so I think it makes sense to stick with this with
convention for the payloads.
And I would like it to be clear that it's for the constraint validators. We might want to introduce other payloads in the future.
Obviously, let's be consistent and update all the other places with this name.
* | ||
* @return {@code this} following the chaining method pattern | ||
* | ||
* @since 6.0.5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's target 6.0.8 now that 6.0.7 has been released.
@gsmet thanks for your feedback! I will update the pull request asap - I am very busy these days. |
@mkurz ah ah, funny thing: i had on my TODO list for today to check if you were still interested in pursuing this. Let me know if you need any help or if you have questions! |
@gsmet I finally found time to update this pull request. Please check the commits I added (the last one probably is the important one).
|
return hibernateSpecificConfig.getConstraintValidatorPayload(); | ||
} | ||
} | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gsmet question: Here I am simply returning null
and not trying to get a result out of the properties
(like in getTemporalValidationTolerance(...)
above. Is that OK/enough here? As I understand looking into the properties
is the way to handle the xml config, however does it make sense to define a payload in the xml at all? If so it would probably just be of type string (even tough the payload could be "object") Do you know what I mean? (Its late... 😉)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, no need to implement it in the properties. It does not make much sense.
I simplified the parameters of the method as we don't need the properties
at all.
@mkurz Hi, about that build failure I think it's because of this error in javadoc:
there's a mismatch between the param name in javadoc |
@marko-bekhta Thanks! I fixed the problem (squashed the fix into the last commit were it belongs) |
b11891d
to
12dcfb8
Compare
@gsmet Updated! Removed |
Alright, it's green now.... |
@mkurz thanks, I'll take it from here (I would like to add something to the documentation about this feature but I still need to figure out where). I'll try to get this done this week so that we can merge it before the end of the week. |
@gsmet Great! Thank you very much for your help 👍 Looking forward to the next release 😛 |
* @since 6.0.8 | ||
*/ | ||
@Incubating | ||
HibernateValidatorConfiguration constraintValidatorPayload(Object constraintValidatorPayload); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let's do it then. I think the confusion stems from the very generic nature of this thing (just passing in and getting out any object). For the originally discussed use case (locale), it doesn't make really sense on the VF, but yes, perhaps for others.
On your environment example, can you detail that a bit, i.e. when would validation be different per environment?
* @return an instance of the specified type or null if no payload of this type has been set | ||
* @since 6.0.8 | ||
*/ | ||
<C> C getPayload(Class<C> type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to say that it should be named getConstraintValidatorPayload()
for the sake of consistency with the other new API methods. But actually I'm wondering whether this all should be named "payload" at all. That term already is used in the Bean Validation API (payload()
on constraints, and also HibernateConstraintViolation#getDynamicPayload()
.
These existing payloads are exposed on the resulting constraint violation. So my question is should we a) use another name here (if it's about some contextual information meant for the constraint validator information) or b) expose that new payload information on resulting constraint violations as we do for the other payloads (which are scoped to a single constraint and its violations, though).
I feel finding another name might be the best thing to avoid confusion with the existing payload concepts (of course a given validator implementation can still decide to forward this new contextual information as a dynamic constraint payload in emitted violations).
Any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @gsmet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gunnarmorling well I asked on HipChat and you told me to name it whatever I feel it to be the most appropriate so that's what I did :). Considering we are in the validator context, I think the context is clear enough, that's why I went for the short form. I couldn't think of another payload we would want it here. I don't have a strong opinion though as I hesitated between the consistency and the impression of redundancy. So if you prefer the full name, let's name it that way, I'm +0 on this.
As for the payload term, I think it's the appropriate generic term for this sort of things so I don't think it's an issue to use it there and I don't think it collides with the other payloads as it's a very generic term.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked on HipChat and you told me to name it whatever I feel it to be the most appropriate so that's what I did :)
Well, of course I was expecting you to do a wise choice ;)
My concern is that using the same name may lead people to expect that it will automatically be exposed via getDynamicPayload()
, which won't be the case unless a specific validator implementation decides to do so. The problem is though I can't think of another good name either, apart from yet another "context" flavor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think payload is a more appropriate term than context.
Let's rename getPayload()
to getConstraintValidatorPayload()
and be extra clear in the documentation I still have to write :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One other thing to note is that it's only exposed in the initialization context passed to the validator. It's not exposed in the isValid()
context. That might help with the confusion as only people willing to use the advanced initialize()
method will notice it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, getConstraintValidatorPayload()
it is. I see it's all marked as incubating, so we got some liberty to adjust down the road, if needed. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, @gsmet will you do the renaming when you write the docs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright!
6f4e8d7
to
274dcea
Compare
@gunnarmorling @mkurz I added a commit with some documentation. Feedback welcome. Once the dust is settled on the documentation, we can (finally) merge this feature. Thanks. |
@gsmet Had a quick look at the documentation you added, LGTM. |
---- | ||
==== | ||
|
||
Once you have set the constraint validator payload, it is time to use it in your constrait validators as shown in the example below: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/constrait/constraint/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
---- | ||
==== | ||
|
||
`HibernateConstraintValidatorInitializationContext#getConstraintValidatorPayload()` has a type parameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question on that one, what happens if I have registered two payloads of type A
and B
, B
extends A
and I request A
? Is an order of precedence defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand your question right you can't have two payloads, because the last one wins.
the constraint violation raised. | ||
|
||
The whole purpose of this constraint validator payload is to be used to condition the behavior of your constraint validators. | ||
It is not included in the constraint violations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
, unless a specific ConstraintValidator
implementation passes on the payload to emitted constraint violations by calling xyz.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I leave that up to @gsmet if he wants to mention that.
.unwrap( HibernateValidatorFactory.class ); | ||
|
||
Validator validator = hibernateValidatorFactory.usingContext() | ||
.constraintValidatorPayload( "US" ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I'm seeing the API used, I'm wondering whether it should be constraintValidatorPayload( String.class, "US" )
. I can see two advantages:
- It makes it clearer that there can be multiple payloads (for different types)
- One can register a given implementation type under the key of a (public) interface
Btw. what happens if I register multiple payloads of the same type? Last one wins, or is an exception raised?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum, no, it seems the implementation actually doesn't allow what I had in mind. Wondering whether that's not a bit too limited?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had the exact same discussion about the other payloads recently and you answered me the user could use a Map or a bean of some sort.
I don't think it's limited and it's consistent with the other payloads in the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...and you answered me the user could use a Map or a bean of some sort.
I don't think it's limited and it's consistent with the other payloads in the API.
I fully agree.
Rebased and applied. Also added a commit for some JavaDoc adjustments. Thanks, everyone! |
🎉 Woohoo! Thanks everyone for your feedback and the time you invested! You do a real great job 👍 😄 |
@mkurz thanks for your contribution. |
@mkurz just a heads up that we released 6.0.8.Final today with the constraint validator payload feature. Time to use in the Play framework :). |
@gsmet Thanks for letting me know! Hopefully next week I will find time to set up a pull request for Play 👍 |
@gsmet I tried to make use the hibernate constraint validator payload feature within the Play Framework this week, however I ran into an issue with the caching behaviour of |
@marko-bekhta and @gsmet probably are the persons who should take a look at this 😉
All the information can be found here: https://hibernate.atlassian.net/browse/HV-1529
Basically we can now pass a dynamic payload to a
(Hibernate)ConstraintValidator
via:And retrieve it via
getDynamicPayload
: