diff --git a/grails-databinding-core/src/main/groovy/grails/databinding/FrameworkPropertyNames.java b/grails-databinding-core/src/main/groovy/grails/databinding/FrameworkPropertyNames.java
new file mode 100644
index 00000000000..89f5c81469d
--- /dev/null
+++ b/grails-databinding-core/src/main/groovy/grails/databinding/FrameworkPropertyNames.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package grails.databinding;
+
+import java.util.Set;
+
+/**
+ * Property names managed by the language runtime or Grails rather than ordinary request data.
+ *
+ * Intrinsic runtime properties are never request-bindable. Grails-managed domain properties
+ * are excluded from generated allowlists by default, but may still bind and be cleared when an
+ * application explicitly opts them in (for example {@code bindable: true}); intrinsic runtime
+ * properties remain protected.
+ */
+public final class FrameworkPropertyNames {
+
+ /**
+ * Language / MetaClass properties that must never be bound from request data.
+ */
+ public static final Set INTRINSIC_RUNTIME_PROPERTIES = Set.of(
+ "class", "classLoader", "protectionDomain", "metaClass", "metaPropertyValues", "properties");
+
+ /**
+ * Grails domain lifecycle properties excluded from default binding allowlists but eligible
+ * for {@code clearMissing} when explicitly included.
+ */
+ public static final Set GRAILS_MANAGED_PROPERTIES = Set.of(
+ "errors", "id", "version", "dateCreated", "lastUpdated");
+
+ /**
+ * Union of {@link #INTRINSIC_RUNTIME_PROPERTIES} and {@link #GRAILS_MANAGED_PROPERTIES}.
+ */
+ public static final Set FRAMEWORK_MANAGED_PROPERTIES = Set.of(
+ "class", "classLoader", "protectionDomain", "metaClass", "metaPropertyValues", "properties",
+ "errors", "id", "version", "dateCreated", "lastUpdated");
+
+ private FrameworkPropertyNames() {
+ }
+}
diff --git a/grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy b/grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy
index ff51e81f5a8..c84fe8b77b2 100755
--- a/grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy
+++ b/grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy
@@ -271,19 +271,25 @@ 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 + '.') })
+ // Only intrinsic runtime properties are hard-denied here. Grails-managed domain
+ // properties (id, version, dateCreated, lastUpdated, errors) may still bind when
+ // explicitly allowlisted (e.g. bindable: true); intrinsic runtime properties remain
+ // hard-denied while Grails-managed properties follow the explicit binding allowlist.
+ !FrameworkPropertyNames.INTRINSIC_RUNTIME_PROPERTIES.contains(propName) && !blackList?.contains(propName) &&
+ (whiteList == null || isBindAllBindingIncludeList(whiteList) || whiteList.contains(propName) ||
+ whiteList.any { item -> item?.toString()?.startsWith(propName + '.') })
}
/**
* Marker include list meaning "bind every eligible property". Used when an
* explicit exclude-only bind must not intersect the class allowlist.
*/
- static List getBindAllBindingIncludeList() {
+ protected static List getBindAllBindingIncludeList() {
BIND_ALL_BINDING_INCLUDE_LIST
}
- static boolean isBindAllBindingIncludeList(List includeList) {
- includeList instanceof BindAllBindingIncludeList
+ protected static boolean isBindAllBindingIncludeList(List includeList) {
+ includeList.is(BIND_ALL_BINDING_INCLUDE_LIST)
}
private static final class BindAllBindingIncludeList extends ArrayList {
diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
index a33dc7de530..bd34bb094ab 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@@ -826,6 +826,9 @@ The Spring annotations still work, so this is non-blocking, but new code should
* **Namespaced link generation is namespace-aware.**
When a link, form action, pagination link, sortable column link, redirect, chain, or include targets a controller without an explicit `namespace`, Grails now resolves the namespace automatically. In the normal case, where only one controller has the target name, `controller` and `action` generate the correct namespaced or non-namespaced URL. Ambiguity only occurs when multiple controllers share the same name. In that case, specify `namespace` to choose the target explicitly. Pass `namespace: null` from Groovy code or `namespace=""` in a GSP tag to target the non-namespaced controller explicitly.
+* **`bindData` can clear omitted included fields.**
+`bindData(target, source, [include: [...], clearMissing: true])` now clears included properties that are absent from the binding source. The behavior is opt-in, requires an `include` list, and does not apply globally. Existing `bindData` calls without `clearMissing: true` keep omitted fields unchanged.
+
==== 21. Tag Library Test Cleanup Changes
Grails 8 removes the `purgeTagLibMetaClass` test hook used by some web and TagLib unit tests.
diff --git a/grails-doc/src/en/ref/Controllers/bindData.adoc b/grails-doc/src/en/ref/Controllers/bindData.adoc
index ec42df40178..cfa329b70f9 100644
--- a/grails-doc/src/en/ref/Controllers/bindData.adoc
+++ b/grails-doc/src/en/ref/Controllers/bindData.adoc
@@ -45,6 +45,9 @@ bindData(target, params, [exclude: ['firstName', 'lastName']], "author")
// using inclusive map
bindData(target, params, [include: ['firstName', 'lastName']], "author")
+
+// clear included properties omitted from the source
+bindData(target, params, [include: ['firstName', 'lastName'], clearMissing: true])
----
@@ -57,7 +60,7 @@ Arguments:
* `target` - The target object to bind to
* `params` - A `Map` of source parameters, often the link:params.html[params] object when used in a controller
-* `includesExcludes` - (Optional) A map with 'include' and/or 'exclude' lists containing the names of properties to either include or exclude.
+* `includesExcludes` - (Optional) A map with 'include' and/or 'exclude' lists containing the names of properties to either include or exclude. Set `clearMissing: true` with an `include` list to clear included properties that are omitted from the binding source.
* `prefix` - (Optional) A string representing a prefix to use to filter parameters. The method will automatically append a '.' when matching the prefix to parameters, so you can use 'author' to filter for parameters such as 'author.name'.
If no `include` list is supplied, `bindData` uses the target class default binding behavior. By default, statically typed instance properties bind for compatibility unless they are marked `bindable: false`. Existing `bindable: true` declarations and explicit `include` lists continue to bind exactly the properties they name without configuration changes. An empty `include` list binds no properties.
@@ -73,6 +76,19 @@ Use `include` to allow only the properties needed for a request:
bindData(target, params, [include: ['firstName', 'lastName']])
----
+Include entries may use wildcard suffixes for nested properties. `address.*` includes all
+properties nested under `address`, while `address_*` is the corresponding underscore-form
+used for nested binding paths and generated binding allowlists:
+
+[source,groovy]
+----
+bindData(target, params, [include: ['address.*']])
+bindData(target, params, [include: ['address_*']])
+----
+
+When `clearMissing: true` is used, omitted properties matched by either wildcard form are
+cleared, subject to the normal `exclude` and bindability rules.
+
For controller action command object parameters, use `grails.web.databinding.BindAllowed` to allow request binding for only the listed properties:
[source,groovy]
@@ -88,6 +104,8 @@ class PersonController {
See the link:{constraintsRefFromRef}bindable.html[bindable] constraint documentation for more information on controlling default bindability. Applications may set `grails.databinding.denyByDefault=true` to opt into deny-by-default binding allowlists. In secure mode, permit a property with `bindable: true`, an explicit `include` list, or `@BindAllowed` on a controller action command object parameter.
+`clearMissing` is opt-in and only applies when an `include` list is provided. This is useful for update forms where an omitted allowed field should clear an existing value instead of leaving stale persisted data. Excluded properties are not cleared.
+
Only boolean values and the strings `'true'` and `'false'` are recognised for `grails.databinding.denyByDefault`. String matching ignores case and surrounding whitespace. An unrecognised value logs a warning and enables secure binding.
The underlying implementation uses Spring's Data Binding framework. If the target is a domain class, type conversion errors are stored in the `errors` property of the domain class.
diff --git a/grails-test-suite-web/src/test/groovy/org/grails/web/binding/DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec.groovy b/grails-test-suite-web/src/test/groovy/org/grails/web/binding/DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec.groovy
index 1f1e1d115d9..e3912796196 100644
--- a/grails-test-suite-web/src/test/groovy/org/grails/web/binding/DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec.groovy
+++ b/grails-test-suite-web/src/test/groovy/org/grails/web/binding/DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec.groovy
@@ -18,41 +18,44 @@
*/
package org.grails.web.binding
+import groovy.transform.CompileStatic
+
+import spock.lang.Issue
+import spock.lang.Specification
+
import grails.config.Settings
import grails.gorm.dirty.checking.DirtyCheck
import grails.persistence.Entity
import grails.util.Holders
-import groovy.transform.CompileStatic
+import grails.web.databinding.DataBindingUtils
+import grails.web.databinding.GrailsWebDataBinder
import org.grails.config.PropertySourcesConfig
import org.grails.validation.ConstraintEvalUtils
-import spock.lang.Issue
-import spock.lang.Specification
-class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends
- Specification {
+class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends Specification {
private def originalConfig
def setup() {
ConstraintEvalUtils.clearDefaultConstraints()
originalConfig = Holders.config
- Holders.setConfig(new PropertySourcesConfig([(Settings.DATABINDING_DENY_BY_DEFAULT): true]))
- grails.web.databinding.DataBindingUtils.clearBindingCaches()
- grails.web.databinding.GrailsWebDataBinder.resetWarnedBindingShapes()
+ Holders.config = new PropertySourcesConfig([(Settings.DATABINDING_DENY_BY_DEFAULT): true])
+ DataBindingUtils.clearBindingCaches()
+ GrailsWebDataBinder.resetWarnedBindingShapes()
}
def cleanup() {
ConstraintEvalUtils.clearDefaultConstraints()
- Holders.setConfig(originalConfig)
- grails.web.databinding.DataBindingUtils.clearBindingCaches()
- grails.web.databinding.GrailsWebDataBinder.resetWarnedBindingShapes()
+ Holders.config = originalConfig
+ DataBindingUtils.clearBindingCaches()
+ GrailsWebDataBinder.resetWarnedBindingShapes()
}
@Issue('GRAILS-11173')
void 'Test binding to special properties in a domain class'() {
when:
- Date now = new Date()
- SomeDomainClass obj = new SomeDomainClass(dateCreated: now, lastUpdated: now)
+ def now = new Date()
+ def obj = new SomeDomainClass(dateCreated: now, lastUpdated: now)
then:
obj.dateCreated == null
@@ -140,8 +143,7 @@ class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends
void 'Test unconfigured binding remains permissive and preserves bindable false'() {
given:
- def configuredConfig = Holders.config
- Holders.setConfig(null)
+ Holders.config = null
when:
def obj = new DomainWithSecureBindableDefault(name: 'Grace', title: 'Admiral', role: 'Admin')
@@ -150,9 +152,6 @@ class DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec extends
obj.name == 'Grace'
obj.title == 'Admiral'
obj.role == null
-
- cleanup:
- Holders.setConfig(configuredConfig)
}
@Issue('https://github.com/apache/grails-core/issues/15795')
diff --git a/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy b/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy
index 8e9e15e9103..d47bda908ed 100644
--- a/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy
+++ b/grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy
@@ -43,7 +43,7 @@ class BindDataMethodTests extends Specification implements ControllerUnitTest children = []
+ Map contacts = [:]
+ ProtectedAddress protectedAddress = new ProtectedAddress()
+ List protectedChildren = []
}
@@ -958,10 +1383,69 @@ class ExistingAllowlistCommandObject {
}
}
+class ComplexPropertyWildcardSiblingCommandObject {
+ Address foo = new Address()
+ String foo_admin
+
+ static constraints = {
+ foo bindable: true
+ }
+}
+
class NoAllowlistCommandObject {
String username
}
+class NoAllowlistCollectionCommandObject {
+ List children = []
+}
+
+class NoAllowlistChild {
+ String name
+ Integer age
+}
+
+class FrameworkManagedCommandObject {
+ Long id
+ Long version
+ Date dateCreated
+ Date lastUpdated
+ String errors
+}
+
+class PrimitiveCommandObject {
+ boolean active
+}
+
+class FailingClearCommandObject {
+ private String currentValue = 'existing'
+
+ String getValue() {
+ currentValue
+ }
+
+ void setValue(String value) {
+ throw new IllegalStateException('value cannot be cleared')
+ }
+}
+
+class ErrorCollectingCommandObject implements Validateable {
+ Integer count
+ private String currentValue = 'existing'
+
+ String getValue() {
+ currentValue
+ }
+
+ void setValue(String value) {
+ throw new IllegalStateException('value cannot be cleared')
+ }
+}
+
+class NestedFailingClearCommandObject {
+ FailingClearCommandObject child
+}
+
class RecordingGrailsWebDataBinder extends GrailsWebDataBinder {
final List warnings = []
@@ -979,3 +1463,53 @@ class RecordingGrailsWebDataBinder extends GrailsWebDataBinder {
warnings << message
}
}
+
+class Child {
+ String name
+ Integer age
+
+ static constraints = {
+ name bindable: true
+ age bindable: true
+ }
+}
+
+class Contact {
+ String type
+ String value
+
+ static constraints = {
+ type bindable: true
+ value bindable: true
+ }
+}
+
+class ProtectedCommandObject {
+ String visible
+ String protectedValue
+
+ static constraints = {
+ visible bindable: true
+ protectedValue bindable: false
+ }
+}
+
+class ProtectedAddress {
+ String country
+ String secret
+
+ static constraints = {
+ country bindable: true
+ secret bindable: false
+ }
+}
+
+class ProtectedChild {
+ String name
+ String secret
+
+ static constraints = {
+ name bindable: true
+ secret bindable: false
+ }
+}
diff --git a/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy b/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy
index 68412393022..cddf7c6d193 100644
--- a/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy
+++ b/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy
@@ -72,7 +72,8 @@ trait DataBinder {
includeList = [DefaultASTDatabindingHelper.NO_BINDABLE_PROPERTIES]
}
List excludeList = convertToListIfCharSequence(includeExclude?.exclude)
- DataBindingUtils.bindObjectToInstance(target, bindingSource, includeList, excludeList, filter)
+ boolean clearMissing = includeExclude?.clearMissing == true
+ DataBindingUtils.bindObjectToInstance(target, bindingSource, includeList, excludeList, filter, clearMissing)
}
@Generated
diff --git a/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java b/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java
index c3e7dd008b2..e5adb5d30f8 100644
--- a/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java
+++ b/grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java
@@ -52,7 +52,6 @@
import grails.databinding.CollectionDataBindingSource;
import grails.databinding.DataBinder;
import grails.databinding.DataBindingSource;
-import grails.databinding.SimpleDataBinder;
import grails.util.Environment;
import grails.util.Holders;
import grails.validation.ValidationErrors;
@@ -515,15 +514,33 @@ public static void bindToCollection(final Class 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);
+ }
+
+ /**
+ * Binds source values and optionally clears omitted explicitly included properties after binding.
+ *
+ * @param object The object to bind to
+ * @param source The source object
+ * @param include The explicit list of properties to include. {@code clearMissing} is ignored when this is {@code null}
+ * @param exclude The list of properties to exclude. Excluded and {@code bindable: false} properties are never cleared
+ * @param filter The prefix to filter by
+ * @param clearMissing Whether omitted explicitly included properties should be cleared after normal binding completes
+ * @return A BindingResult for request/body or binding exceptions. When clearing is active, it also contains normal
+ * binding errors already stored on the target and null-clearing failures, or null when no such errors occur.
+ * Clearing runs after normal binder listeners have completed and does not emit listener callbacks.
+ */
+ public static BindingResult bindObjectToInstance(Object object, Object source, List include, List exclude, String filter, boolean clearMissing) {
+ boolean explicitInclude = include != null;
if (include == null) {
if (exclude == null || isDenyByDefaultEnabled()) {
include = getBindingIncludeList(object);
} else {
// Exclude-only in compatibility mode must not intersect the class allowlist.
- include = SimpleDataBinder.getBindAllBindingIncludeList();
+ include = GrailsWebDataBinder.bindAllBindingIncludeList();
}
}
- else if (include.isEmpty() && !SimpleDataBinder.isBindAllBindingIncludeList(include)) {
+ else if (include.isEmpty() && !GrailsWebDataBinder.isBindAllIncludeList(include)) {
include = Collections.singletonList(DefaultASTDatabindingHelper.NO_BINDABLE_PROPERTIES);
}
GrailsApplication application = Holders.findApplication();
@@ -535,7 +552,7 @@ else if (include.isEmpty() && !SimpleDataBinder.isBindAllBindingIncludeList(incl
//no-op
}
}
- return bindObjectToDomainInstance(entity, object, source, include, exclude, filter);
+ return bindObjectToDomainInstance(entity, object, source, include, exclude, filter, clearMissing && explicitInclude);
}
/**
@@ -559,10 +576,41 @@ public static BindingResult bindObjectToDomainInstance(PersistentEntity entity,
if (exclude == null || isDenyByDefaultEnabled()) {
include = getBindingIncludeList(object);
} else {
- include = SimpleDataBinder.getBindAllBindingIncludeList();
+ include = GrailsWebDataBinder.bindAllBindingIncludeList();
+ }
+ }
+ else if (include.isEmpty() && !GrailsWebDataBinder.isBindAllIncludeList(include)) {
+ include = Collections.singletonList(DefaultASTDatabindingHelper.NO_BINDABLE_PROPERTIES);
+ }
+ return bindObjectToDomainInstance(entity, object, source, include, exclude, filter, false);
+ }
+
+ /**
+ * Binds source values to a domain instance and optionally clears omitted explicitly included properties after binding.
+ *
+ * @param entity The persistent entity metadata, if available
+ * @param object The object to bind to
+ * @param source The source object
+ * @param include The explicit non-null list of properties to include. {@code clearMissing} is ignored when this is {@code null}
+ * @param exclude The list of properties to exclude. Excluded and {@code bindable: false} properties are never cleared
+ * @param filter The prefix to filter by
+ * @param clearMissing Whether omitted explicitly included properties should be cleared after normal binding completes
+ * @return A BindingResult for request/body or binding exceptions. When clearing is active, it also contains normal
+ * binding errors already stored on the target and null-clearing failures, or null when no such errors occur.
+ * Clearing runs after normal binder listeners have completed and does not emit listener callbacks.
+ */
+ @SuppressWarnings("unchecked")
+ public static BindingResult bindObjectToDomainInstance(PersistentEntity entity, Object object,
+ Object source, List include, List exclude, String filter, boolean clearMissing) {
+ boolean explicitInclude = include != null;
+ if (include == null) {
+ if (exclude == null || isDenyByDefaultEnabled()) {
+ include = getBindingIncludeList(object);
+ } else {
+ include = GrailsWebDataBinder.bindAllBindingIncludeList();
}
}
- else if (include.isEmpty() && !SimpleDataBinder.isBindAllBindingIncludeList(include)) {
+ else if (include.isEmpty() && !GrailsWebDataBinder.isBindAllIncludeList(include)) {
include = Collections.singletonList(DefaultASTDatabindingHelper.NO_BINDABLE_PROPERTIES);
}
BindingResult bindingResult = null;
@@ -572,6 +620,21 @@ else if (include.isEmpty() && !SimpleDataBinder.isBindAllBindingIncludeList(incl
final DataBindingSource bindingSource = createDataBindingSource(grailsApplication, object.getClass(), source);
final DataBinder grailsWebDataBinder = getGrailsWebDataBinder(grailsApplication);
grailsWebDataBinder.bind(object, bindingSource, filter, include, exclude);
+ if (clearMissing && explicitInclude && !include.isEmpty()) {
+ BeanPropertyBindingResult clearMissingResult = new BeanPropertyBindingResult(object, object.getClass().getName());
+ MetaClass targetMetaClass = GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+ if (targetMetaClass.hasProperty(object, "errors") != null) {
+ Object existingErrors = targetMetaClass.getProperty(object, "errors");
+ if (existingErrors instanceof BindingResult) {
+ clearMissingResult.addAllErrors((BindingResult) existingErrors);
+ }
+ }
+ MissingPropertyClearer.clearMissingIncludedProperties(
+ object, bindingSource, include, exclude, filter, clearMissingResult);
+ if (clearMissingResult.hasErrors()) {
+ bindingResult = clearMissingResult;
+ }
+ }
} catch (InvalidRequestBodyException e) {
String messageCode = "invalidRequestBody";
Class objectType = object.getClass();
diff --git a/grails-web-databinding/src/main/groovy/grails/web/databinding/GrailsWebDataBinder.groovy b/grails-web-databinding/src/main/groovy/grails/web/databinding/GrailsWebDataBinder.groovy
index 89725ae02a3..e559583b342 100644
--- a/grails-web-databinding/src/main/groovy/grails/web/databinding/GrailsWebDataBinder.groovy
+++ b/grails-web-databinding/src/main/groovy/grails/web/databinding/GrailsWebDataBinder.groovy
@@ -42,6 +42,7 @@ import org.springframework.validation.ObjectError
import grails.core.GrailsApplication
import grails.databinding.BindingFormat
import grails.databinding.DataBindingSource
+import grails.databinding.FrameworkPropertyNames
import grails.databinding.SimpleDataBinder
import grails.databinding.SimpleMapDataBindingSource
import grails.databinding.TypedStructuredBindingEditor
@@ -83,10 +84,6 @@ class GrailsWebDataBinder extends SimpleDataBinder {
private static final Logger LOG = LoggerFactory.getLogger(GrailsWebDataBinder)
private static final int MAX_WARNED_BINDING_SHAPES = 1024
private static final Set WARNED_BINDING_SHAPES = new LinkedHashSet<>()
- private static final Set FRAMEWORK_MANAGED_PROPERTIES = [
- 'class', 'errors', 'id', 'version', 'dateCreated', 'lastUpdated'
- ] as Set
-
protected GrailsApplication grailsApplication
protected MessageSource messageSource
boolean trimStrings = true
@@ -146,12 +143,20 @@ class GrailsWebDataBinder extends SimpleDataBinder {
if (includeList == null) {
return getBindingIncludeList(object)
}
- if (includeList.isEmpty() && !isBindAllBindingIncludeList(includeList)) {
+ if (includeList.isEmpty() && !isBindAllIncludeList(includeList)) {
return [DefaultASTDatabindingHelper.NO_BINDABLE_PROPERTIES]
}
includeList
}
+ static List bindAllBindingIncludeList() {
+ SimpleDataBinder.getBindAllBindingIncludeList()
+ }
+
+ static boolean isBindAllIncludeList(List includeList) {
+ SimpleDataBinder.isBindAllBindingIncludeList(includeList)
+ }
+
@Override
protected void doBind(object, DataBindingSource source, String filter, List whiteList, List blackList, DataBindingListener listener, errors) {
def observationRegistry = resolveObservationRegistry()
@@ -243,7 +248,7 @@ class GrailsWebDataBinder extends SimpleDataBinder {
boolean allowed = super.isOkToBind(property, whiteList, blackList)
if (!allowed && DataBindingUtils.isGeneratedBindingIncludeList(whiteList) &&
super.isOkToBind(property, null, blackList) &&
- !FRAMEWORK_MANAGED_PROPERTIES.contains(property.name)) {
+ !FrameworkPropertyNames.FRAMEWORK_MANAGED_PROPERTIES.contains(property.name)) {
warnAboutIgnoredBindingProperty(bindingTargetType.get(), property.name)
}
allowed
@@ -634,7 +639,7 @@ class GrailsWebDataBinder extends SimpleDataBinder {
instance = referencedType.getDeclaredConstructor().newInstance()
} catch (NoSuchMethodException | IllegalAccessException ignored) {
if (value instanceof Map) {
- if (isBindAllBindingIncludeList(includeList) ||
+ if (isBindAllIncludeList(includeList) ||
!DataBindingUtils.isDenyByDefaultEnabled()) {
return referencedType.newInstance(filterUnbindableMapConstructorArguments(referencedType, (Map) value))
}
diff --git a/grails-web-databinding/src/main/groovy/grails/web/databinding/MissingPropertyClearer.java b/grails-web-databinding/src/main/groovy/grails/web/databinding/MissingPropertyClearer.java
new file mode 100644
index 00000000000..2a031bf929c
--- /dev/null
+++ b/grails-web-databinding/src/main/groovy/grails/web/databinding/MissingPropertyClearer.java
@@ -0,0 +1,823 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package grails.web.databinding;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import groovy.lang.GroovySystem;
+import groovy.lang.MetaClass;
+import groovy.lang.MetaProperty;
+
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+
+import grails.databinding.DataBindingSource;
+import grails.databinding.FrameworkPropertyNames;
+
+import static grails.web.databinding.DataBindingUtils.addUnbindablePropertyNames;
+import static grails.web.databinding.DataBindingUtils.getBindingIncludeList;
+
+/**
+ * Clears explicitly included properties omitted from a binding source.
+ *
+ * @since 8.0
+ */
+final class MissingPropertyClearer {
+
+ private static final String BLANK = "";
+ private static final ThreadLocal BINDING_RESULT = new ThreadLocal<>();
+ private static final ThreadLocal CLEAR_PATH = new ThreadLocal<>();
+
+ private MissingPropertyClearer() {
+ }
+
+ static void clearMissingIncludedProperties(Object object, DataBindingSource bindingSource, List include, List exclude,
+ String filter, BindingResult bindingResult) {
+ List expandedInclude = expandWildcardIncludes(object, include);
+ BindingResult previousBindingResult = BINDING_RESULT.get();
+ BINDING_RESULT.set(bindingResult);
+ try {
+ for (Object includedProperty : expandedInclude) {
+ if (includedProperty instanceof CharSequence) {
+ String propertyName = includedProperty.toString();
+ if (isClearMissingPropertyBindable(object, propertyName, expandedInclude, exclude)) {
+ if (assignNullToMissingIndexedProperties(object, bindingSource, propertyName, filter)) {
+ continue;
+ }
+ if (!bindingSourceContainsProperty(bindingSource, propertyName, filter)) {
+ setPropertyToNull(object, propertyName);
+ }
+ }
+ }
+ }
+ }
+ finally {
+ if (previousBindingResult == null) {
+ BINDING_RESULT.remove();
+ }
+ else {
+ BINDING_RESULT.set(previousBindingResult);
+ }
+ }
+ }
+
+ private static List expandWildcardIncludes(Object object, List include) {
+ List expandedInclude = new ArrayList();
+ for (Object includedProperty : include) {
+ if (!(includedProperty instanceof CharSequence)) {
+ expandedInclude.add(includedProperty);
+ continue;
+ }
+ String propertyName = includedProperty.toString();
+ if (propertyName.endsWith(".*")) {
+ expandNestedWildcard(object, propertyName.substring(0, propertyName.length() - 2), expandedInclude);
+ }
+ else if (propertyName.endsWith("_*")) {
+ expandSiblingWildcard(object, propertyName.substring(0, propertyName.length() - 2), expandedInclude);
+ }
+ else {
+ expandedInclude.add(propertyName);
+ }
+ }
+ return expandedInclude;
+ }
+
+ private static void expandNestedWildcard(Object object, String propertyPath, List expandedInclude) {
+ Object nestedObject = getPropertyValue(object, propertyPath);
+ if (nestedObject instanceof Collection) {
+ int index = 0;
+ for (Object item : (Collection) nestedObject) {
+ expandObjectProperties(item, propertyPath + "[" + index + "]", expandedInclude);
+ index++;
+ }
+ }
+ else if (nestedObject instanceof Map) {
+ for (Object entryObject : ((Map) nestedObject).entrySet()) {
+ Map.Entry entry = (Map.Entry) entryObject;
+ expandObjectProperties(entry.getValue(), propertyPath + "[" + entry.getKey() + "]", expandedInclude);
+ }
+ }
+ else {
+ expandObjectProperties(nestedObject, propertyPath, expandedInclude);
+ }
+ }
+
+ private static void expandSiblingWildcard(Object object, String propertyPrefix, List expandedInclude) {
+ for (Object property : getMetaProperties(object)) {
+ String propertyName = ((MetaProperty) property).getName();
+ if (propertyName.startsWith(propertyPrefix + "_")) {
+ expandedInclude.add(propertyName);
+ }
+ }
+ }
+
+ private static void expandObjectProperties(Object object, String propertyPath, List expandedInclude) {
+ if (object == null) {
+ return;
+ }
+ for (Object property : getMetaProperties(object)) {
+ expandedInclude.add(propertyPath + "." + ((MetaProperty) property).getName());
+ }
+ }
+
+ private static List getMetaProperties(Object object) {
+ if (object == null) {
+ return Collections.emptyList();
+ }
+ MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+ return metaClass.getProperties();
+ }
+
+ private static boolean isClearMissingPropertyBindable(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 (!isClearMissingPropertyPathAllowed(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 && !isClearMissingPropertyBindable(item, nestedPropertyName, getNestedIncludeList(include, propertyName), null)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (nestedObject instanceof Map) {
+ for (Object value : ((Map) nestedObject).values()) {
+ if (value != null && !isClearMissingPropertyBindable(value, nestedPropertyName, getNestedIncludeList(include, propertyName), null)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return nestedObject == null || isClearMissingPropertyBindable(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 isPropertyExcluded(String propertyName, List excludeList) {
+ if (excludeList == null) {
+ return false;
+ }
+ for (Object item : excludeList) {
+ String excludeName = item == null ? null : item.toString();
+ if (excludeName != null && (excludeName.equals(propertyName) || propertyName.startsWith(excludeName + ".") ||
+ (excludeName.endsWith(".*") && propertyName.startsWith(excludeName.substring(0, excludeName.length() - 1))) ||
+ (excludeName.endsWith("_*") && propertyName.startsWith(excludeName.substring(0, excludeName.length() - 1))))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isClearMissingPropertyPathAllowed(String propertyName, List generatedIncludeList, List explicitIncludeList, List excludeList) {
+ if (isIntrinsicRuntimeProperty(propertyName) || isPropertyExcluded(propertyName, excludeList)) {
+ return false;
+ }
+ return isClearMissingPropertyIncluded(propertyName, generatedIncludeList) ||
+ isClearMissingPropertyIncluded(propertyName, explicitIncludeList);
+ }
+
+ private static boolean isIntrinsicRuntimeProperty(String propertyName) {
+ int separator = propertyPathSeparator(propertyName);
+ String rootPropertyName = separator == -1 ? propertyName : propertyName.substring(0, separator);
+ return FrameworkPropertyNames.INTRINSIC_RUNTIME_PROPERTIES.contains(rootPropertyName);
+ }
+
+ private static boolean isClearMissingPropertyIncluded(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 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 getIndexedSourcePropertyNames(Object source, String indexedSourcePropertyPrefix) {
+ Set 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 getSourcePropertyNames(Object source) {
+ Set 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 previousClearPath = CLEAR_PATH.get();
+ CLEAR_PATH.set(propertyName);
+ try {
+ 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]);
+ }
+ }
+ finally {
+ if (previousClearPath == null) {
+ CLEAR_PATH.remove();
+ }
+ else {
+ CLEAR_PATH.set(previousClearPath);
+ }
+ }
+ }
+
+ 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 (RuntimeException e) {
+ addClearError(propertyName, 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) {
+ int bracket = propertyName.indexOf('[');
+ try {
+ if (bracket == -1) {
+ MetaClass mc = GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+ MetaProperty metaProperty = mc.hasProperty(object, propertyName);
+ if (metaProperty != null) {
+ mc.setProperty(object, propertyName,
+ metaProperty.getType().isPrimitive() ? primitiveDefault(metaProperty.getType()) : null);
+ }
+ return;
+ }
+
+ MetaClass mc = GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
+ Object indexedProperty = mc.getProperty(object, propertyName.substring(0, bracket));
+ String index = propertyName.substring(bracket + 1, propertyName.indexOf(']', bracket));
+ if (indexedProperty instanceof List) {
+ List list = (List) indexedProperty;
+ Integer parsedIndex = parseIndex(index);
+ if (parsedIndex != null && parsedIndex >= 0 && parsedIndex < list.size()) {
+ list.set(parsedIndex, null);
+ }
+ }
+ else if (indexedProperty instanceof Map) {
+ ((Map) indexedProperty).put(index, null);
+ }
+ }
+ catch (RuntimeException e) {
+ addClearError(propertyName, e);
+ }
+ }
+
+ private static void addClearError(String propertyName, RuntimeException exception) {
+ BindingResult bindingResult = BINDING_RESULT.get();
+ if (bindingResult == null) {
+ return;
+ }
+ String clearPath = CLEAR_PATH.get();
+ String field = clearPath == null ? propertyName : clearPath;
+ bindingResult.addError(new FieldError(bindingResult.getObjectName(), field, null, true,
+ new String[] { "typeMismatch." + field, "typeMismatch" }, null,
+ "Failed to clear omitted included property: " + exception.getMessage()));
+ }
+
+ private static Object primitiveDefault(Class primitiveType) {
+ if (Boolean.TYPE.equals(primitiveType)) {
+ return false;
+ }
+ if (Character.TYPE.equals(primitiveType)) {
+ return Character.valueOf('\0');
+ }
+ if (Byte.TYPE.equals(primitiveType)) {
+ return Byte.valueOf((byte) 0);
+ }
+ if (Short.TYPE.equals(primitiveType)) {
+ return Short.valueOf((short) 0);
+ }
+ if (Integer.TYPE.equals(primitiveType)) {
+ return Integer.valueOf(0);
+ }
+ if (Long.TYPE.equals(primitiveType)) {
+ return Long.valueOf(0L);
+ }
+ if (Float.TYPE.equals(primitiveType)) {
+ return Float.valueOf(0F);
+ }
+ if (Double.TYPE.equals(primitiveType)) {
+ return Double.valueOf(0D);
+ }
+ return null;
+ }
+
+ private static Integer parseIndex(String index) {
+ try {
+ return Integer.valueOf(index);
+ }
+ catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+}