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


##########
grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy:
##########
@@ -133,6 +133,158 @@ class BindDataMethodTests extends Specification 
implements ControllerUnitTest<Bi
         target.email == null
     }
 
+    void 'Test bindData With Null Missing Clears Omitted Included Field'() {

Review Comment:
   This spec and every other `nullMissing` case in the file inherit 
`legacyBindableDefault=false` from the `setup()` at the top, so the whole 
feature is only exercised in the opt-in secure mode.
   
   The reference docs and the 8.0.x upgrade note both present `nullMissing` as 
a plain `bindData` option with no mention of the binding mode, and the 
authorization path genuinely differs between the two: 
`isNullMissingPropertyBindable` consults `getBindingIncludeList(object)`, which 
resolves the legacy allowlist under the default and the generated one under 
secure mode.
   
   Please add default-mode coverage for the main paths — clearing an omitted 
included field, leaving an excluded one, honouring `bindable: false`, and the 
nested/indexed cases — so the documented behavior is verified in the mode most 
applications will be running.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -77,6 +82,9 @@ public class DataBindingUtils {
     private static final String BLANK = "";
     private static final Map<Class, List> CLASS_TO_BINDING_INCLUDE_LIST = new 
ConcurrentHashMap<>();
     private static final Map<Class, List> CLASS_TO_LEGACY_BINDING_INCLUDE_LIST 
= new ConcurrentHashMap<>();
+    private static final Set<String> FRAMEWORK_MANAGED_PROPERTIES = Set.of(

Review Comment:
   This is now the third list of "properties the binder must never touch" in 
the stack, and the three disagree:
   
   - `SimpleDataBinder.isFrameworkProperty`: `class, classLoader, 
protectionDomain, metaClass, metaPropertyValues, properties`
   - `GrailsWebDataBinder.FRAMEWORK_MANAGED_PROPERTIES`: `class, errors, id, 
version, dateCreated, lastUpdated`
   - this one: the union of both
   
   A property added to one and not the others becomes clearable but not 
bindable, or the reverse. Please consolidate to a single constant that all 
three consume.



##########
grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy:
##########
@@ -271,7 +271,23 @@ class SimpleDataBinder implements DataBinder {
     }
 
     protected boolean isOkToBind(String propName, List whiteList, List 
blackList) {
-        'class' != propName && 'classLoader' != propName && 'protectionDomain' 
!= propName && 'metaClass' != propName && 'metaPropertyValues' != propName && 
'properties' != propName && !blackList?.contains(propName) && (whiteList == 
null || isBindAllBindingIncludeList(whiteList) || whiteList.contains(propName) 
|| whiteList.find { it -> it?.toString()?.startsWith(propName + '.') })
+        !isFrameworkProperty(propName) && !blackList?.contains(propName) &&
+                (whiteList == null || isBindAllBindingIncludeList(whiteList) 
|| whiteList.contains(propName) ||
+                        whiteList.any { item -> 
item?.toString()?.startsWith(propName + '.') })
+    }
+
+    static boolean isPropertyExcluded(String propertyName, List excludeList) {

Review Comment:
   This adds a public static method to `grails.databinding.SimpleDataBinder` — 
public API in `grails-databinding-core` — solely so `DataBindingUtils` in 
`grails-web-databinding` can call it. It also implements exclusion semantics 
(`.*` and `_*` prefix matching, and nested-path prefixes) that the binder's own 
`isOkToBind` does not apply, so `SimpleDataBinder` now exposes two different 
notions of "excluded".
   
   It has no javadoc and no coverage in `SimpleDataBinderSpec`. If it is only 
needed by the web binding layer, please make it package-private there or move 
it to a shared internal utility; if it is genuinely meant to be public API, 
document it and test it at the `SimpleDataBinder` level.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -479,6 +487,11 @@ public static <T> void bindToCollection(final Class<T> 
targetType, final Collect
      * @return A BindingResult if there were errors or null if it was 
successful
      */
     public static BindingResult bindObjectToInstance(Object object, Object 
source, List include, List exclude, String filter) {
+        return bindObjectToInstance(object, source, include, exclude, filter, 
false);
+    }
+
+    public static BindingResult bindObjectToInstance(Object object, Object 
source, List include, List exclude, String filter, boolean nullMissing) {

Review Comment:
   This overload and the matching `bindObjectToDomainInstance(..., boolean 
nullMissing)` are new public API on a class where the neighbouring overloads 
are documented.
   
   Please add javadoc covering the parameter, the fact that it is ignored 
unless the caller supplied a non-null `include`, and that the clearing happens 
after binding completes — outside the listener callbacks and outside the 
`BindingResult` the method returns.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -576,6 +605,619 @@ else if (include.isEmpty()) {
         return bindingResult;
     }
 
+    private static void assignNullToMissingIncludedProperties(Object object, 
DataBindingSource bindingSource, List include, List exclude, String filter) {

Review Comment:
   This is where the cost of the design shows up. 
`assignNullToMissingIncludedProperties` re-walks the binding source after 
`grailsWebDataBinder.bind(...)` has returned, and to do that it re-implements 
path splitting (`splitPropertyPath`, `propertyPathSeparator`), bracket and 
index parsing, checkbox marker names, collection and map expansion, and 
prefix-filter handling — a second implementation of the grammar the binder just 
traversed with `IndexedPropertyReferenceDescriptor` and 
`processIndexedProperty`.
   
   Two implementations of the same path grammar will drift, and this is the 
copy that decides which properties get written to. `shouldExpandMapEntries` / 
`isStructuredMapValueType` inferring "is this a nested object map?" from the 
package name of the value type is a symptom of working without the type 
information the binder had already resolved.
   
   Could the clearing be driven from inside the binder instead — it already 
holds the resolved allowlist and knows which properties it set, so the 
remainder is the set to null? If that is not workable, please extract this into 
its own collaborator with its own unit tests rather than growing 
`DataBindingUtils`, which is already carrying the include-list resolution and 
the mode switch.



##########
grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java:
##########
@@ -576,6 +605,619 @@ else if (include.isEmpty()) {
         return bindingResult;
     }
 
+    private static void assignNullToMissingIncludedProperties(Object object, 
DataBindingSource bindingSource, List include, List exclude, String filter) {
+        for (Object includedProperty : include) {
+            if (includedProperty instanceof CharSequence) {
+                String propertyName = includedProperty.toString();
+                if (propertyName.indexOf('*') == -1 && 
isNullMissingPropertyBindable(object, propertyName, include, exclude)) {
+                    if (assignNullToMissingIndexedProperties(object, 
bindingSource, propertyName, filter)) {
+                        continue;
+                    }
+                    if (!bindingSourceContainsProperty(bindingSource, 
propertyName, filter)) {
+                        setPropertyToNull(object, propertyName);
+                    }
+                }
+            }
+        }
+    }
+
+    private static boolean isNullMissingPropertyBindable(Object object, String 
propertyName, List include, List exclude) {
+        if (object == null) {
+            return false;
+        }
+        String allowlistPropertyName = removePropertyIndexes(propertyName);
+        List bindingIncludeList = getBindingIncludeList(object);
+        List bindingExcludeList = 
normalizePropertyIndexes(addUnbindablePropertyNames(object, exclude));
+        if (!isNullMissingPropertyPathAllowed(allowlistPropertyName, 
bindingIncludeList, include, bindingExcludeList)) {
+            return false;
+        }
+        int separator = propertyPathSeparator(propertyName);
+        if (separator == -1) {
+            return true;
+        }
+
+        Object nestedObject = getPropertyValue(object, 
propertyName.substring(0, separator));
+        String nestedPropertyName = propertyName.substring(separator + 1);
+        if (nestedObject instanceof Collection) {
+            for (Object item : (Collection) nestedObject) {
+                if (item != null && !isNullMissingPropertyBindable(item, 
nestedPropertyName, getNestedIncludeList(include, propertyName), null)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        if (nestedObject instanceof Map) {
+            for (Object value : ((Map) nestedObject).values()) {
+                if (value != null && !isNullMissingPropertyBindable(value, 
nestedPropertyName, getNestedIncludeList(include, propertyName), null)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return nestedObject == null || 
isNullMissingPropertyBindable(nestedObject, nestedPropertyName, 
getNestedIncludeList(include, propertyName), null);
+    }
+
+    private static String removePropertyIndexes(String propertyName) {
+        return propertyName.replaceAll("\\[[^]]*]", "");
+    }
+
+    private static List normalizePropertyIndexes(List propertyNames) {
+        if (propertyNames == null) {
+            return Collections.emptyList();
+        }
+        List normalizedPropertyNames = new ArrayList(propertyNames.size());
+        for (Object propertyName : propertyNames) {
+            normalizedPropertyNames.add(propertyName instanceof CharSequence ? 
removePropertyIndexes(propertyName.toString()) : propertyName);
+        }
+        return normalizedPropertyNames;
+    }
+
+    private static List getNestedIncludeList(List include, String 
propertyName) {
+        if (include == null || include.isEmpty()) {
+            return Collections.emptyList();
+        }
+        String normalizedPropertyName = removePropertyIndexes(propertyName);
+        int separator = propertyPathSeparator(normalizedPropertyName);
+        if (separator == -1) {
+            return Collections.emptyList();
+        }
+        String rootPropertyName = normalizedPropertyName.substring(0, 
separator);
+        List nestedIncludeList = new ArrayList();
+        for (Object includedProperty : include) {
+            if (includedProperty instanceof CharSequence) {
+                String includedPropertyName = 
removePropertyIndexes(includedProperty.toString());
+                int includedPropertySeparator = 
propertyPathSeparator(includedPropertyName);
+                if (includedPropertySeparator != -1 && 
rootPropertyName.equals(includedPropertyName.substring(0, 
includedPropertySeparator))) {
+                    
nestedIncludeList.add(includedPropertyName.substring(includedPropertySeparator 
+ 1));
+                }
+            }
+        }
+        return nestedIncludeList;
+    }
+
+    private static boolean isNullMissingPropertyPathAllowed(String 
propertyName, List generatedIncludeList, List explicitIncludeList, List 
excludeList) {
+        if (isFrameworkManagedProperty(propertyName) || 
SimpleDataBinder.isPropertyExcluded(propertyName, excludeList)) {
+            return false;
+        }
+        return isNullMissingPropertyIncluded(propertyName, 
generatedIncludeList) ||
+                isNullMissingPropertyIncluded(propertyName, 
explicitIncludeList);
+    }
+
+    private static boolean isFrameworkManagedProperty(String propertyName) {
+        int separator = propertyPathSeparator(propertyName);
+        String rootPropertyName = separator == -1 ? propertyName : 
propertyName.substring(0, separator);
+        return FRAMEWORK_MANAGED_PROPERTIES.contains(rootPropertyName);
+    }
+
+    private static boolean isNullMissingPropertyIncluded(String propertyName, 
List includeList) {
+        if (includeList == null) {
+            return false;
+        }
+        for (Object includedProperty : includeList) {
+            if (includedProperty instanceof CharSequence) {
+                String includedPropertyName = 
removePropertyIndexes(includedProperty.toString());
+                if (includedPropertyName.equals(propertyName)) {
+                    return true;
+                }
+                if (includedPropertyName.endsWith(".*")) {
+                    String prefix = includedPropertyName.substring(0, 
includedPropertyName.length() - 2);
+                    if (propertyName.startsWith(prefix + ".")) {
+                        return true;
+                    }
+                }
+                if (includedPropertyName.endsWith("_*")) {
+                    String prefix = includedPropertyName.substring(0, 
includedPropertyName.length() - 2);
+                    if (propertyName.startsWith(prefix + ".") || 
propertyName.startsWith(prefix + "_")) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private static boolean assignNullToMissingIndexedProperties(Object object, 
DataBindingSource bindingSource, String propertyName, String filter) {
+        String sourcePropertyName = filter == null ? propertyName : filter + 
"." + propertyName;
+        return assignNullToMissingIndexedProperties(object, bindingSource, 
BLANK, sourcePropertyName, propertyName);
+    }
+
+    private static boolean assignNullToMissingIndexedProperties(Object object, 
Object source, String targetPathPrefix, String sourcePropertyName, String 
targetPropertyName) {
+        int sourceSeparator = propertyPathSeparator(sourcePropertyName);
+        int targetSeparator = propertyPathSeparator(targetPropertyName);
+        if (sourceSeparator == -1 || targetSeparator == -1) {
+            return false;
+        }
+
+        String sourceRootPropertyName = sourcePropertyName.substring(0, 
sourceSeparator);
+        String targetRootPropertyName = targetPropertyName.substring(0, 
targetSeparator);
+        String nestedSourcePropertyName = 
sourcePropertyName.substring(sourceSeparator + 1);
+        String nestedTargetPropertyName = 
targetPropertyName.substring(targetSeparator + 1);
+        String[] sourceSegments = splitPropertyPath(sourcePropertyName);
+        String[] targetSegments = splitPropertyPath(targetPropertyName);
+        if (sourceSegments.length > targetSegments.length) {
+            if (containsSourceProperty(source, sourceRootPropertyName)) {
+                return assignNullToMissingIndexedProperties(object, 
getSourcePropertyValue(source, sourceRootPropertyName), targetPathPrefix, 
nestedSourcePropertyName, targetPropertyName);
+            }
+            int sourceRootSegmentCount = sourceSegments.length - 
targetSegments.length + 1;
+            sourceRootPropertyName = joinPropertyPath(sourceSegments, 0, 
sourceRootSegmentCount);
+            nestedSourcePropertyName = joinPropertyPath(sourceSegments, 
sourceRootSegmentCount, sourceSegments.length);
+            targetRootPropertyName = targetSegments[0];
+            nestedTargetPropertyName = joinPropertyPath(targetSegments, 1, 
targetSegments.length);
+        }
+
+        if (containsSourceProperty(source, sourceRootPropertyName)) {
+            Object nestedSource = getSourcePropertyValue(source, 
sourceRootPropertyName);
+            if (nestedSource instanceof Collection) {
+                return assignNullToMissingCollectionProperties(object, 
(Collection) nestedSource, targetPathPrefix, targetRootPropertyName, 
nestedSourcePropertyName, nestedTargetPropertyName);
+            }
+            Object targetObject = getTargetObject(object, targetPathPrefix);
+            if (nestedSource instanceof Map && hasNestedSourceEntries((Map) 
nestedSource) && shouldExpandMapEntries(targetObject, targetObject == null ? 
null : targetObject.getClass(), targetRootPropertyName)) {
+                return assignNullToMissingMapProperties(object, (Map) 
nestedSource, targetPathPrefix, targetRootPropertyName, 
nestedSourcePropertyName, nestedTargetPropertyName);
+            }
+        }
+
+        boolean indexed = false;
+        String indexedSourcePropertyPrefix = sourceRootPropertyName + "[";
+        for (String indexedSourcePropertyName : 
getIndexedSourcePropertyNames(source, indexedSourcePropertyPrefix)) {
+            indexed = true;
+            String targetIndexedPropertyName = 
appendPropertyPath(targetPathPrefix, targetRootPropertyName + 
indexedSourcePropertyName.substring(sourceRootPropertyName.length()));
+            if (containsSourceProperty(source, indexedSourcePropertyName)) {
+                Object nestedSource = getSourcePropertyValue(source, 
indexedSourcePropertyName);
+                if (!assignNullToMissingIndexedProperties(object, 
nestedSource, targetIndexedPropertyName, nestedSourcePropertyName, 
nestedTargetPropertyName) && !containsPropertyPath(nestedSource, 
nestedSourcePropertyName)) {
+                    setPropertyToNull(object, targetIndexedPropertyName + "." 
+ nestedTargetPropertyName);
+                }
+            }
+            else if (!containsPropertyPath(source, indexedSourcePropertyName + 
"." + nestedSourcePropertyName)) {
+                setPropertyToNull(object, targetIndexedPropertyName + "." + 
nestedTargetPropertyName);
+            }
+        }
+        return indexed;
+    }
+
+    private static boolean assignNullToMissingCollectionProperties(Object 
object, Collection collection, String targetPathPrefix, String 
targetRootPropertyName, String nestedSourcePropertyName, String 
nestedTargetPropertyName) {
+        int index = 0;
+        for (Object item : collection) {
+            String targetIndexedPropertyName = 
appendPropertyPath(targetPathPrefix, targetRootPropertyName + "[" + index + 
"]");
+            assignNullToMissingNestedProperty(object, item, 
targetIndexedPropertyName, nestedSourcePropertyName, nestedTargetPropertyName);
+            index++;
+        }
+        return true;
+    }
+
+    private static boolean assignNullToMissingMapProperties(Object object, Map 
map, String targetPathPrefix, String targetRootPropertyName, String 
nestedSourcePropertyName, String nestedTargetPropertyName) {
+        for (Object entryObject : map.entrySet()) {
+            Map.Entry entry = (Map.Entry) entryObject;
+            String targetIndexedPropertyName = 
appendPropertyPath(targetPathPrefix, targetRootPropertyName + "[" + 
entry.getKey() + "]");
+            assignNullToMissingNestedProperty(object, entry.getValue(), 
targetIndexedPropertyName, nestedSourcePropertyName, nestedTargetPropertyName);
+        }
+        return true;
+    }
+
+    private static void assignNullToMissingNestedProperty(Object object, 
Object nestedSource, String targetIndexedPropertyName, String 
nestedSourcePropertyName, String nestedTargetPropertyName) {
+        if (!assignNullToMissingIndexedProperties(object, nestedSource, 
targetIndexedPropertyName, nestedSourcePropertyName, nestedTargetPropertyName) 
&& !containsPropertyPath(nestedSource, nestedSourcePropertyName)) {
+            setPropertyToNull(object, targetIndexedPropertyName + "." + 
nestedTargetPropertyName);
+        }
+    }
+
+    private static String joinPropertyPath(String[] segments, int start, int 
end) {
+        StringBuilder propertyPath = new StringBuilder();
+        for (int i = start; i < end; i++) {
+            if (propertyPath.length() > 0) {
+                propertyPath.append('.');
+            }
+            propertyPath.append(segments[i]);
+        }
+        return propertyPath.toString();
+    }
+
+    private static boolean bindingSourceContainsProperty(DataBindingSource 
bindingSource, String propertyName, String filter) {
+        String sourcePropertyName = filter == null ? propertyName : filter + 
"." + propertyName;
+        int exactPrefixSegments = filter == null ? 0 : 
splitPropertyPath(filter).length;
+        return containsPropertyPath(bindingSource, sourcePropertyName, 
exactPrefixSegments) || containsPropertyPath(bindingSource, 
checkboxMarkerPropertyName(sourcePropertyName), exactPrefixSegments);
+    }
+
+    private static boolean containsPropertyPath(Object source, String 
propertyName) {
+        return containsPropertyPath(source, propertyName, 0);
+    }
+
+    private static boolean containsPropertyPath(Object source, String 
propertyName, int exactPrefixSegments) {
+        if (containsSourceProperty(source, propertyName)) {
+            return true;
+        }
+        if (containsIndexedPropertyPath(source, propertyName, 
exactPrefixSegments)) {
+            return true;
+        }
+        int separator = propertyPathSeparator(propertyName);
+        if (separator == -1) {
+            return false;
+        }
+        String rootPropertyName = propertyName.substring(0, separator);
+        if (!containsSourceProperty(source, rootPropertyName)) {
+            return containsIndexedNestedPropertyPath(source, rootPropertyName, 
propertyName.substring(separator + 1));
+        }
+        Object nestedSource = getSourcePropertyValue(source, rootPropertyName);
+        String nestedPropertyName = propertyName.substring(separator + 1);
+        if (nestedSource instanceof Collection) {
+            for (Object item : (Collection) nestedSource) {
+                if (containsPropertyPath(item, nestedPropertyName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return containsPropertyPath(nestedSource, nestedPropertyName);
+    }
+
+    private static boolean containsIndexedNestedPropertyPath(Object source, 
String rootPropertyName, String nestedPropertyName) {
+        String indexedSourcePropertyPrefix = rootPropertyName + "[";
+        for (String indexedSourcePropertyName : 
getIndexedSourcePropertyNames(source, indexedSourcePropertyPrefix)) {
+            if (containsSourceProperty(source, indexedSourcePropertyName) && 
containsPropertyPath(getSourcePropertyValue(source, indexedSourcePropertyName), 
nestedPropertyName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean containsIndexedPropertyPath(Object source, String 
propertyName, int exactPrefixSegments) {
+        for (String indexedPropertyName : getSourcePropertyNames(source)) {
+            if (indexedPropertyPathMatches(indexedPropertyName, propertyName, 
exactPrefixSegments)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static int propertyPathSeparator(String propertyName) {
+        return propertyPathSeparator(propertyName, false);
+    }
+
+    private static int propertyPathSeparator(String propertyName, boolean 
last) {
+        int separator = -1;
+        int bracketDepth = 0;
+        for (int i = 0; i < propertyName.length(); i++) {
+            char character = propertyName.charAt(i);
+            if (character == '[') {
+                bracketDepth++;
+            }
+            else if (character == ']' && bracketDepth > 0) {
+                bracketDepth--;
+            }
+            else if (character == '.' && bracketDepth == 0) {
+                if (!last) {
+                    return i;
+                }
+                separator = i;
+            }
+        }
+        return separator;
+    }
+
+    private static String[] splitPropertyPath(String propertyName) {
+        List<String> segments = new ArrayList<>();
+        StringBuilder segment = new StringBuilder();
+        int bracketDepth = 0;
+        for (int i = 0; i < propertyName.length(); i++) {
+            char character = propertyName.charAt(i);
+            if (character == '.' && bracketDepth == 0) {
+                segments.add(segment.toString());
+                segment.setLength(0);
+            }
+            else {
+                if (character == '[') {
+                    bracketDepth++;
+                }
+                else if (character == ']' && bracketDepth > 0) {
+                    bracketDepth--;
+                }
+                segment.append(character);
+            }
+        }
+        segments.add(segment.toString());
+        return segments.toArray(new String[0]);
+    }
+
+    private static boolean indexedPropertyPathMatches(String 
indexedPropertyName, String propertyName, int exactPrefixSegments) {
+        String[] indexedPropertySegments = 
splitPropertyPath(indexedPropertyName);
+        String[] propertySegments = splitPropertyPath(propertyName);
+        if (indexedPropertySegments.length != propertySegments.length) {
+            return false;
+        }
+        for (int i = 0; i < propertySegments.length; i++) {
+            if (indexedPropertySegments[i].equals(propertySegments[i])) {
+                continue;
+            }
+            if (i < exactPrefixSegments) {
+                return false;
+            }
+            if (!indexedSegmentMatches(indexedPropertySegments[i], 
propertySegments[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static boolean indexedSegmentMatches(String indexedSegment, String 
segment) {
+        return indexedSegment.startsWith(segment + "[") && 
indexedSegment.endsWith("]");
+    }
+
+    private static Set<String> getIndexedSourcePropertyNames(Object source, 
String indexedSourcePropertyPrefix) {
+        Set<String> indexedSourcePropertyNames = new LinkedHashSet<>();
+        for (String propertyName : getSourcePropertyNames(source)) {
+            if (propertyName.startsWith(indexedSourcePropertyPrefix)) {
+                int closingIndex = propertyName.indexOf(']', 
indexedSourcePropertyPrefix.length());
+                if (closingIndex > -1) {
+                    indexedSourcePropertyNames.add(propertyName.substring(0, 
closingIndex + 1));
+                }
+            }
+        }
+        return indexedSourcePropertyNames;
+    }
+
+    private static boolean containsSourceProperty(Object source, String 
propertyName) {
+        if (source instanceof DataBindingSource) {
+            return ((DataBindingSource) source).containsProperty(propertyName);
+        }
+        if (source instanceof Map) {
+            return ((Map) source).containsKey(propertyName);
+        }
+        return false;
+    }
+
+    private static Set<String> getSourcePropertyNames(Object source) {
+        Set<String> propertyNames = new LinkedHashSet<>();
+        if (source instanceof DataBindingSource) {
+            propertyNames.addAll(((DataBindingSource) 
source).getPropertyNames());
+        }
+        else if (source instanceof Map) {
+            for (Object key : ((Map) source).keySet()) {
+                propertyNames.add(key.toString());
+            }
+        }
+        return propertyNames;
+    }
+
+    private static Object getSourcePropertyValue(Object source, String 
propertyName) {
+        if (source instanceof DataBindingSource) {
+            return ((DataBindingSource) source).getPropertyValue(propertyName);
+        }
+        return ((Map) source).get(propertyName);
+    }
+
+    private static String checkboxMarkerPropertyName(String propertyName) {
+        int separator = propertyPathSeparator(propertyName, true);
+        if (separator == -1) {
+            return "_" + propertyName;
+        }
+        return propertyName.substring(0, separator + 1) + "_" + 
propertyName.substring(separator + 1);
+    }
+
+    private static boolean hasNestedSourceEntries(Map map) {
+        for (Object value : map.values()) {
+            if (value instanceof Map || value instanceof Collection || value 
instanceof DataBindingSource) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean shouldExpandMapEntries(Object target, Class 
targetType, String propertyName) {
+        Object value = getTargetPropertyValue(target, propertyName);
+        if (value instanceof Map && hasStructuredTargetMapValues((Map) value)) 
{
+            return true;
+        }
+
+        Class mapValueType = getMapValueType(target, targetType, propertyName);
+        return mapValueType != null && isStructuredMapValueType(mapValueType);
+    }
+
+    private static boolean hasStructuredTargetMapValues(Map map) {
+        for (Object value : map.values()) {
+            if (value != null && isStructuredMapValueType(value.getClass())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean isStructuredMapValueType(Class valueType) {
+        Package valuePackage = valueType.getPackage();
+        return !valueType.isPrimitive() &&
+            (valuePackage == null || 
!valuePackage.getName().startsWith("java.")) &&
+            !CharSequence.class.isAssignableFrom(valueType) &&
+            !Number.class.isAssignableFrom(valueType) &&
+            !Boolean.class.isAssignableFrom(valueType) &&
+            !Enum.class.isAssignableFrom(valueType) &&
+            !Map.class.isAssignableFrom(valueType) &&
+            !Collection.class.isAssignableFrom(valueType) &&
+            !Object.class.equals(valueType);
+    }
+
+    private static Class getMapValueType(Object target, Class targetType, 
String propertyName) {
+        Class resolvedTargetType = target == null ? targetType : 
target.getClass();
+        if (resolvedTargetType == null) {
+            return null;
+        }
+
+        MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(resolvedTargetType);
+        MetaProperty metaProperty = mc.getMetaProperty(propertyName);
+        if (metaProperty == null || 
!Map.class.isAssignableFrom(metaProperty.getType())) {
+            return null;
+        }
+
+        Field field = findField(resolvedTargetType, propertyName);
+        if (field == null) {
+            return null;
+        }
+        return getMapValueType(field.getGenericType());
+    }
+
+    private static Class getMapValueType(Type type) {
+        if (!(type instanceof ParameterizedType)) {
+            return null;
+        }
+
+        Type[] typeArguments = ((ParameterizedType) 
type).getActualTypeArguments();
+        if (typeArguments.length < 2) {
+            return null;
+        }
+        Type valueType = typeArguments[1];
+        if (valueType instanceof Class) {
+            return (Class) valueType;
+        }
+        if (valueType instanceof ParameterizedType && ((ParameterizedType) 
valueType).getRawType() instanceof Class) {
+            return (Class) ((ParameterizedType) valueType).getRawType();
+        }
+        return null;
+    }
+
+    private static Field findField(Class type, String propertyName) {
+        Class currentType = type;
+        while (currentType != null) {
+            try {
+                return currentType.getDeclaredField(propertyName);
+            }
+            catch (NoSuchFieldException e) {
+                currentType = currentType.getSuperclass();
+            }
+        }
+        return null;
+    }
+
+    private static Object getTargetObject(Object object, String 
targetPathPrefix) {
+        if (targetPathPrefix == null || targetPathPrefix.length() == 0) {
+            return object;
+        }
+
+        Object targetObject = object;
+        for (String propertyName : splitPropertyPath(targetPathPrefix)) {
+            if (targetObject == null) {
+                return null;
+            }
+            targetObject = getPropertyValue(targetObject, propertyName);
+        }
+        return targetObject;
+    }
+
+    private static Object getTargetPropertyValue(Object target, String 
propertyName) {
+        if (target == null) {
+            return null;
+        }
+
+        try {
+            MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(target.getClass());
+            return mc.getProperty(target, propertyName);
+        }
+        catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static String appendPropertyPath(String parentPath, String 
propertyName) {
+        if (parentPath == null || parentPath.length() == 0) {
+            return propertyName;
+        }
+        return parentPath + "." + propertyName;
+    }
+
+    private static void setPropertyToNull(Object object, String propertyName) {
+        String[] propertyNames = splitPropertyPath(propertyName);
+        Object currentObject = object;
+        for (int i = 0; i < propertyNames.length - 1 && currentObject != null; 
i++) {
+            currentObject = getPropertyValue(currentObject, propertyNames[i]);
+        }
+        if (currentObject != null) {
+            setPropertyValueToNull(currentObject, 
propertyNames[propertyNames.length - 1]);
+        }
+    }
+
+    private static Object getPropertyValue(Object object, String propertyName) 
{
+        int bracket = propertyName.indexOf('[');
+        try {
+            if (bracket == -1) {
+                MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+                return mc.getProperty(object, propertyName);
+            }
+
+            MetaClass mc = 
GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+            Object indexedProperty = mc.getProperty(object, 
propertyName.substring(0, bracket));
+            return getIndexedValue(indexedProperty, 
propertyName.substring(bracket + 1, propertyName.indexOf(']', bracket)));
+        }
+        catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static Object getIndexedValue(Object indexedProperty, String 
index) {
+        if (indexedProperty instanceof List) {
+            List list = (List) indexedProperty;
+            Integer parsedIndex = parseIndex(index);
+            return parsedIndex != null && parsedIndex >= 0 && parsedIndex < 
list.size() ? list.get(parsedIndex) : null;
+        }
+        if (indexedProperty instanceof Map) {
+            return ((Map) indexedProperty).get(index);
+        }
+        return null;
+    }
+
+    private static void setPropertyValueToNull(Object object, String 
propertyName) {

Review Comment:
   Every failure here is swallowed. A `nullMissing` clear that cannot be 
applied — a primitive-typed property, a property with no setter, a setter that 
throws — silently leaves the stale value in place, which is exactly the outcome 
the option exists to prevent, and nothing is recorded in the `BindingResult` 
that `bindData` returns.
   
   The catch comment says "ignore invalid indexed nullMissing paths", but the 
`try` also covers the non-indexed `mc.setProperty(...)` branch, so it is 
broader than the comment claims.
   
   Please narrow the catch to the path/index parsing, decide explicitly what a 
primitive-typed include should do (reject it at the API, or set the type 
default), and surface a failure to clear through the errors object the way a 
binding failure is surfaced. A test with a primitive property in the include 
list would pin whichever behavior you choose.



-- 
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