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]