jdaugherty commented on code in PR #16781:
URL: https://github.com/apache/grails-core/pull/16781#discussion_r4393103167


##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -163,6 +167,56 @@ private static Class bindingType(final Object object) {
         return object.getClass();
     }
 
+    private static Class persistentClass(final Object object, final 
MappingContext mappingContext) {
+        final ProxyHandler proxyHandler = mappingContext.getProxyHandler();
+        if (proxyHandler != null && proxyHandler.isProxy(object)) {
+            final Class proxiedClass = proxyHandler.getProxiedClass(object);
+            if (proxiedClass != null) {
+                return proxiedClass;
+            }
+        }
+        return object.getClass();
+    }
+
+    /**
+     * The constraints of the persistent entity an object is an instance of, 
or a proxy of. The mapping context
+     * evaluates them once, for the validator of the entity, and keeps them 
for as long as the entity is mapped,
+     * so binding reads them there rather than evaluating the {@code 
constraints} closure of the class again.
+     *
+     * @return the constraints, or {@code null} when the object is not a 
mapped entity or its validator does not
+     * expose its constraints
+     */
+    private static Map getEntityConstrainedProperties(final Object object, 
final GrailsApplication application) {
+        if (application == null) {
+            return null;
+        }
+        try {
+            final MappingContext mappingContext = 
application.getMappingContext();

Review Comment:
   This only finds entities of the primary datastore. 
`grailsApplication.mappingContext` is the `grailsDomainClassMappingContext` 
bean, and `MongoDbDataStoreSpringInitializer` registers that alias only when 
MongoDB is not a secondary datastore. A MongoDB entity in a Hibernate app isn't 
found here, so it binds correctly but still evaluates its constraints on every 
bind. The same is true of an entity annotated `@Validated`, whose validator is 
a `GormValidatorAdapter`.
   
   Is it OK to leave these cases for a follow-up issue?



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -163,6 +167,56 @@ private static Class bindingType(final Object object) {
         return object.getClass();
     }
 
+    private static Class persistentClass(final Object object, final 
MappingContext mappingContext) {
+        final ProxyHandler proxyHandler = mappingContext.getProxyHandler();
+        if (proxyHandler != null && proxyHandler.isProxy(object)) {
+            final Class proxiedClass = proxyHandler.getProxiedClass(object);
+            if (proxiedClass != null) {
+                return proxiedClass;
+            }
+        }
+        return object.getClass();
+    }
+
+    /**
+     * The constraints of the persistent entity an object is an instance of, 
or a proxy of. The mapping context
+     * evaluates them once, for the validator of the entity, and keeps them 
for as long as the entity is mapped,
+     * so binding reads them there rather than evaluating the {@code 
constraints} closure of the class again.
+     *
+     * @return the constraints, or {@code null} when the object is not a 
mapped entity or its validator does not
+     * expose its constraints
+     */
+    private static Map getEntityConstrainedProperties(final Object object, 
final GrailsApplication application) {
+        if (application == null) {
+            return null;
+        }
+        try {
+            final MappingContext mappingContext = 
application.getMappingContext();
+            if (mappingContext == null) {
+                return null;
+            }
+            final Class type = persistentClass(object, mappingContext);
+            final PersistentEntity entity = 
mappingContext.getPersistentEntity(type.getName());
+            if (entity == null || entity.getJavaClass() != type) {

Review Comment:
   No test covers either fallback guard. I removed both `entity.getJavaClass() 
!= type` and the `validator == null` check below, and `DataBindingUtilsSpec`, 
`GrailsWebDataBinderSpec` and `DomainProxyBindingSpec` all still pass.
   
   Could you add a test for each?
   
   - A mapped entity that has no validator, for example a 
`KeyValueMappingContext` with `addPersistentEntity` and no 
`addEntityValidator`, should bind with the constraints its class declares. 
Without the guard, `validator.getClass()` throws a `NullPointerException`, 
which nothing here catches, so the bind fails.
   - An object whose class name resolves to an entity that isn't its class. 
This happens after a class is reloaded. It also happens for any class whose 
name contains `_$$_`, because `getPersistentEntity(String)` cuts the name at 
that marker. A class named something like `MappedEntity_$$_Probe`, which the 
proxy handler doesn't recognise and whose own constraints differ from the 
validator's, shows which set was used.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -163,6 +167,56 @@ private static Class bindingType(final Object object) {
         return object.getClass();
     }
 
+    private static Class persistentClass(final Object object, final 
MappingContext mappingContext) {
+        final ProxyHandler proxyHandler = mappingContext.getProxyHandler();
+        if (proxyHandler != null && proxyHandler.isProxy(object)) {
+            final Class proxiedClass = proxyHandler.getProxiedClass(object);
+            if (proxiedClass != null) {
+                return proxiedClass;
+            }
+        }
+        return object.getClass();
+    }
+
+    /**
+     * The constraints of the persistent entity an object is an instance of, 
or a proxy of. The mapping context
+     * evaluates them once, for the validator of the entity, and keeps them 
for as long as the entity is mapped,
+     * so binding reads them there rather than evaluating the {@code 
constraints} closure of the class again.
+     *
+     * @return the constraints, or {@code null} when the object is not a 
mapped entity or its validator does not
+     * expose its constraints
+     */
+    private static Map getEntityConstrainedProperties(final Object object, 
final GrailsApplication application) {
+        if (application == null) {
+            return null;
+        }
+        try {
+            final MappingContext mappingContext = 
application.getMappingContext();
+            if (mappingContext == null) {
+                return null;
+            }
+            final Class type = persistentClass(object, mappingContext);
+            final PersistentEntity entity = 
mappingContext.getPersistentEntity(type.getName());
+            if (entity == null || entity.getJavaClass() != type) {
+                return null;
+            }
+            final Validator validator = 
mappingContext.getEntityValidator(entity);
+            if (validator == null) {
+                return null;
+            }
+            final MetaClass metaClass = 
GroovySystem.getMetaClassRegistry().getMetaClass(validator.getClass());
+            final MetaProperty constrainedProperties = 
metaClass.hasProperty(validator, CONSTRAINED_PROPERTIES);

Review Comment:
   There's already an SPI for reading an entity's constraints: 
`org.grails.validation.discovery.ConstrainedDiscovery` in grails-core. Its 
default implementation, `DefaultConstrainedDiscovery` in grails-validation, 
reads the same validator but checks `instanceof ConstrainedEntity`. Looking up 
a property named `constrainedProperties` finds `PersistentEntityValidator` 
today, but that name isn't a contract. Any validator with a `Map` property of 
that name would be read as constraints.
   
   Can binding go through `ConstrainedDiscovery`, loaded once rather than on 
every bind? It wraps each property in a new `ConstrainedDelegate` on every 
call, so it would need measuring against the numbers in the description. If 
that costs too much, please add a comment here saying so, so the next reader 
knows this copy of the lookup is deliberate.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to