Skip to content

Commit 27b6097

Browse files
authored
Merge pull request #16209 from codeconsole/fix/databinding-raw-collection-8.0.x
Binding empties a raw collection in applications that never opted in to deny-by-default
2 parents e40cb27 + f408864 commit 27b6097

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

grails-test-suite-persistence/src/test/groovy/grails/web/databinding/GrailsWebDataBinderSpec.groovy

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import spock.lang.Unroll
2828
import org.springframework.context.support.StaticMessageSource
2929

3030
import grails.config.Settings
31+
import grails.util.Holders
3132
import grails.databinding.BindUsing
3233
import grails.databinding.BindingFormat
3334
import grails.databinding.DataBindingSource
@@ -51,7 +52,7 @@ class GrailsWebDataBinderSpec extends Specification implements DataTest {
5152
mockDomains(
5253
AssociationBindingAuthor, AssociationBindingBook, AssociationBindingPage, Author, BinderNullabilityEntity,
5354
Child, CollectionContainer, DataBindingBook, Fidget, Foo, GeneratedBindingChild, GeneratedBindingParent,
54-
Parent, Publication, Publisher, Team, Widget
55+
Parent, Publication, Publisher, RawCollectionContainer, Team, Widget
5556
)
5657
}
5758

@@ -1962,6 +1963,96 @@ class GrailsWebDataBinderSpec extends Specification implements DataTest {
19621963
obj.publishers.find { it.name == 'Pub One' }
19631964
obj.publishers.find { it.name == 'Pub Three' }
19641965
}
1966+
1967+
void 'test binding maps into a raw collection preserves the map elements'() {
1968+
given: 'a domain with a raw (non-generic) collection, whose component type falls back to Object'
1969+
def obj = new RawCollectionContainer()
1970+
1971+
when: 'a list of maps is bound to it'
1972+
binder.bind(obj, new SimpleMapDataBindingSource([
1973+
rawList: [[label: 'Answered', param: 'status=resolved'],
1974+
[label: 'Pending', param: 'status=pending']]
1975+
]))
1976+
1977+
then: 'the maps survive binding rather than being replaced by empty Object instances'
1978+
obj.rawList.size() == 2
1979+
obj.rawList.every { it instanceof Map }
1980+
obj.rawList[0].label == 'Answered'
1981+
obj.rawList[0].param == 'status=resolved'
1982+
obj.rawList[1].label == 'Pending'
1983+
obj.rawList[1].param == 'status=pending'
1984+
}
1985+
1986+
void 'test binding maps into a raw Set property preserves the map elements'() {
1987+
given:
1988+
def obj = new RawCollectionContainer()
1989+
1990+
when:
1991+
binder.bind(obj, new SimpleMapDataBindingSource([
1992+
rawSet: [[label: 'Answered', param: 'status=resolved']]
1993+
]))
1994+
1995+
then:
1996+
obj.rawSet.every { it instanceof Map }
1997+
obj.rawSet.first().label == 'Answered'
1998+
}
1999+
2000+
void 'test binding maps into a raw Collection-typed property'() {
2001+
given:
2002+
def obj = new RawCollectionContainer()
2003+
2004+
when:
2005+
binder.bind(obj, new SimpleMapDataBindingSource([rawCollection: [[label: 'Answered']]]))
2006+
2007+
then:
2008+
obj.rawCollection.every { it instanceof Map }
2009+
obj.rawCollection[0].label == 'Answered'
2010+
}
2011+
2012+
void 'test binding DataBindingSource items into a raw collection'() {
2013+
given:
2014+
def obj = new RawCollectionContainer()
2015+
2016+
when:
2017+
binder.bind(obj, new SimpleMapDataBindingSource([
2018+
rawList: [new SimpleMapDataBindingSource([label: 'Answered'])]
2019+
]))
2020+
2021+
then:
2022+
obj.rawList.size() == 1
2023+
obj.rawList[0].getClass() != Object
2024+
}
2025+
2026+
void 'test binding maps into a raw collection with deny-by-default enabled'() {
2027+
given: 'the opt-in hardening turned on, and the property explicitly allowlisted'
2028+
def originalConfig = Holders.config
2029+
Holders.setConfig(new PropertySourcesConfig([(Settings.DATABINDING_DENY_BY_DEFAULT): true]))
2030+
DataBindingUtils.clearBindingCaches()
2031+
def obj = new RawCollectionContainer()
2032+
2033+
when:
2034+
binder.bind(obj, new SimpleMapDataBindingSource([
2035+
rawList: [[label: 'Answered', param: 'status=resolved']]
2036+
]), null, ['rawList'], null, null)
2037+
2038+
then: 'the elements are still maps, as they are with the hardening off'
2039+
obj.rawList.size() == 1
2040+
obj.rawList[0] instanceof Map
2041+
obj.rawList[0].label == 'Answered'
2042+
2043+
cleanup:
2044+
Holders.setConfig(originalConfig)
2045+
DataBindingUtils.clearBindingCaches()
2046+
}
2047+
}
2048+
2049+
@Entity
2050+
class RawCollectionContainer {
2051+
2052+
List rawList = []
2053+
Map rawMap = [:]
2054+
Set rawSet = []
2055+
Collection rawCollection = []
19652056
}
19662057

19672058
@Entity

grails-web-databinding/src/main/groovy/grails/web/databinding/GrailsWebDataBinder.groovy

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ class GrailsWebDataBinder extends SimpleDataBinder {
505505
} else if (Collection.isAssignableFrom(metaProperty.type)) {
506506
def referencedType = getReferencedTypeForCollection(propName, obj)
507507
if (referencedType) {
508-
def listValue
508+
List listValue
509509
if (val instanceof List) {
510510
listValue = (List) val
511511
} else if (val instanceof GPathResultMap && ((GPathResultMap) val).size() == 1) {
@@ -545,7 +545,17 @@ class GrailsWebDataBinder extends SimpleDataBinder {
545545
}
546546
}
547547
if (persistentInstance == null) {
548-
if (item instanceof Map || item instanceof DataBindingSource) {
548+
if (item == null || referencedType.isAssignableFrom(item.getClass())) {
549+
// Already of the element type, so there is nothing to instantiate
550+
// and nothing to bind into. A raw collection always lands here:
551+
// Basic#componentType falls back to Object.class when a property
552+
// carries no generic signature, and Object is assignable from
553+
// everything. The array branch above and the Map branch below ask
554+
// the same question before instantiating; only this one did not,
555+
// so a map element was replaced by an empty Object and its
556+
// contents were dropped.
557+
itemsWhichNeedBinding << item
558+
} else if (item instanceof Map || item instanceof DataBindingSource) {
549559
DataBindingSource itemBindingSource = item instanceof DataBindingSource ?
550560
(DataBindingSource) item : new SimpleMapDataBindingSource((Map) item)
551561
def instance = instantiateAndBindNestedOrUseMapConstructor(

0 commit comments

Comments
 (0)