codeconsole commented on code in PR #16771:
URL: https://github.com/apache/grails-core/pull/16771#discussion_r4418993337
##########
grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy:
##########
@@ -171,6 +177,205 @@ class NavigableMap implements Map<String, Object>,
Cloneable {
}
}
+ /**
+ * Holds the list or map that an indexed key with a property after the
index, such as {@code a.rows[0].name}, writes
+ * in the nested map that owns it, so that {@code navigate('a', 'rows')}
and {@code config.a.rows} read it. The list
+ * the owner already holds is the one held, as written element by element
from application.yml or by a profile's
+ * list over it; only when it holds none is it built from the list under
the flat key. An element that is a map is
+ * held as a nested map, which the owner also holds under the indexed
name, {@code rows[0]}, and which holds the
+ * element as application.yml gives it. Only the element just written is
updated, so each indexed key costs the same
+ * however long the list is. The flat keys are left as they are, so the
environment presents the same names and
+ * values as before.
+ */
+ private void holdIndexedContainer(NavigableMap targetMap, String
indexedKey) {
+ int subscriptStart = indexedKey.indexOf('[')
+ int subscriptEnd = subscriptStart > -1 ? indexedKey.indexOf(']',
subscriptStart) : -1
+ if (subscriptEnd < 0 || indexedKey.indexOf('.', subscriptEnd) < 0) {
+ return
+ }
+ String containerKey = indexedKey.substring(0, subscriptStart)
+ int lastDot = containerKey.lastIndexOf('.')
+ if (lastDot < 0) {
+ // the write of the container has put it in the map that owns it
already
+ return
+ }
+ NavigableMap owner =
targetMap.navigateSubMap(containerKey.substring(0, lastDot).tokenize('.'),
false)
+ if (owner == null) {
+ return
+ }
+ String name = containerKey.substring(lastDot + 1)
+ String written = indexedKey.substring(subscriptStart + 1, subscriptEnd)
+ String writtenField = indexedKey.substring(subscriptEnd + 2)
+ Object container = targetMap.get(containerKey)
+ Object current = owner.get(name)
+ if (container instanceof List) {
+ if (!written.isInteger()) {
+ return
+ }
+ HeldList held
+ if (current instanceof HeldList) {
+ held = (HeldList) current
+ } else {
+ boolean fromFlatKey = !(current instanceof List)
+ List source = fromFlatKey ? (List) container : (List) current
+ held = new HeldList()
+ for (int i = 0; i < source.size(); i++) {
+ String index = String.valueOf(i)
+ Object element = fromFlatKey ? nestedOrElement(owner,
"${name}[${index}]".toString(), source.get(i)) : source.get(i)
+ held.add(index == written ? element :
heldElement(targetMap, owner, containerKey, name, index, element, null))
+ }
+ }
+ int index = written.toInteger()
+ Object element = index < held.size() ? held.get(index) : null
+ if (element == null && index < ((List) container).size()) {
+ element = ((List) container).get(index)
+ }
+ fill(held, index, null)
+ held.set(index, heldElement(targetMap, owner, containerKey, name,
written, element, writtenField))
+ owner.put(name, held)
+ } else if (container instanceof Map) {
+ NavigableMap held
+ if (current instanceof NavigableMap) {
+ held = (NavigableMap) current
+ } else {
+ boolean fromFlatKey = !(current instanceof Map)
+ Map source = fromFlatKey ? (Map) container : (Map) current
+ held = new NavigableMap(owner.getRootConfig(),
(owner.getPath() + [name]).asImmutable())
+ for (Map.Entry entry in ((Map) source).entrySet()) {
+ String key = String.valueOf(entry.key)
+ Object element = fromFlatKey ? nestedOrElement(owner,
"${name}[${key}]".toString(), entry.value) : entry.value
+ held.put(key, key == written ? element :
heldElement(targetMap, owner, containerKey, name, key, element, null))
+ }
+ }
+ Object element = held.containsKey(written) ? held.get(written) :
((Map) container).get(written)
+ held.put(written, heldElement(targetMap, owner, containerKey,
name, written, element, writtenField))
+ owner.put(name, held)
+ }
+ }
+
+ /**
+ * The element to hold, which the owner also holds under its indexed name,
{@code rows[0]}. An element that is a
+ * nested map is kept, and so is the nested map the owner holds for the
element just written when it is that same
+ * map, since the merge has written the field into it, or when the element
is a value that is not a map, which the
+ * map the indexed key wrote replaces, as {@code rows[1].name} replaces
{@code 'two'}. Otherwise an element that is a map is held as a nested map
+ * built from it, so that it holds every field the element holds, such as
{@code db.url} beside the {@code db.port}
+ * just written below it, and none of an element the list held before it
was replaced. The field just written then
+ * takes the value the owner's nested map holds for it, which the merge
stored through
+ * {@link #mergeMapEntry(NavigableMap, String, Object)}, so a placeholder
in it is resolved as in every other entry,
+ * and the keys of the map the indexed key was merged into that held the
element, or a map inside it, such as
+ * {@code a.rows[0]} and {@code a.rows[0].db}, then hold the rebuilt
element and its maps.
+ */
+ private static Object heldElement(NavigableMap targetMap, NavigableMap
owner, String containerKey, String name, String index,
+ Object element, String writtenField) {
+ String indexedName = "${name}[${index}]".toString()
+ Object written = writtenField != null ? owner.get(indexedName) : null
+ if (written instanceof NavigableMap && (element == null ||
element.is(written) || !(element instanceof Map))) {
Review Comment:
Fixed in c33edd5fb0. The map that the first indexed key wrote under
`rows[1]` survived the replacement, and the new field was merged into it. Now,
when the list held a value that is not a map at that index, or the owner held
no list at all, the element is rebuilt from the element under the flat key. It
takes the value the nested map resolved for each of that element's fields, and
a field written before the replacement or the clear is left out. A map left
under the indexed name of an element that is not a map is removed.
After your sequence, `navigate('a', 'rows')` and the flat `a.rows` both give
`[[name: 'first'], [name: 'second']]`. Clearing with null and then writing
`a.rows[0].port` gives `[[port: '8080']]`. The same holds for a map written by
subscript (`a.routes[x]`). `NavigableMapSpec` covers the list replaced and
written in either order, the list cleared at the same index and at another one,
and the map case. The 'replaced with values' and 'cleared, same element' rows
fail at 3f917458a6. The `grails-bootstrap`, `grails-core` and
`grails-web-url-mappings` suites pass.
##########
grails-core/src/main/groovy/org/grails/config/NavigableMapConfig.java:
##########
@@ -330,6 +339,14 @@ private <T> T convertValueIfNecessary(Object
originalValue, Class<T> targetType,
if (originalValue instanceof NavigableMap &&
targetType.equals(Map.class)) {
final IdentityHashMap<NavigableMap, Map<Object, Object>>
cache = new IdentityHashMap<>();
return (T) convertToMap((NavigableMap) originalValue,
cache);
+ } else if (originalValue instanceof List &&
Collection.class.isAssignableFrom(targetType) && holdsNavigableMap((List<?>)
originalValue)) {
+ // the maps of a list are given as plain maps, as a map is
+ final IdentityHashMap<NavigableMap, Map<Object, Object>>
cache = new IdentityHashMap<>();
+ final List<Object> converted = new ArrayList<>();
Review Comment:
Fixed in 53c8eff37c. The maps of a list are now converted to plain maps
before the type check, whatever type is asked for. If the converted list isn't
an instance of that type, it goes through the conversion service like any other
value. So `getProperty(key, LinkedList)` gives a `LinkedList` of plain maps,
and `Set`, `Iterable` and `Object` give plain maps, as on 7.0.x.
`PropertySourcesConfigSpec` reads a list written by indexed keys, a
`LinkedList` of plain maps and a `LinkedList` of navigable maps as `List`,
`Collection`, `LinkedList`, `ArrayList`, `Set`, `Iterable` and `Object`. It
checks the type and that every element is a `LinkedHashMap`. Your `LinkedList`
case and the indexed-key rows for `Set`, `LinkedList`, `Iterable` and `Object`
fail at 3f917458a6.
##########
grails-core/src/main/groovy/org/grails/config/NavigableMapConfig.java:
##########
@@ -330,6 +339,14 @@ private <T> T convertValueIfNecessary(Object
originalValue, Class<T> targetType,
if (originalValue instanceof NavigableMap &&
targetType.equals(Map.class)) {
final IdentityHashMap<NavigableMap, Map<Object, Object>>
cache = new IdentityHashMap<>();
return (T) convertToMap((NavigableMap) originalValue,
cache);
+ } else if (originalValue instanceof List &&
Collection.class.isAssignableFrom(targetType) && holdsNavigableMap((List<?>)
originalValue)) {
+ // the maps of a list are given as plain maps, as a map is
+ final IdentityHashMap<NavigableMap, Map<Object, Object>>
cache = new IdentityHashMap<>();
+ final List<Object> converted = new ArrayList<>();
+ for (Object o: (List<?>) originalValue) {
+ converted.add(o instanceof NavigableMap ?
convertToMap((NavigableMap) o, cache) : o);
+ }
+ return (T) converted;
Review Comment:
Done in 53c8eff37c: the conversion now runs before the type check, as you
suggested. James asked for the same thing at r4418430586. With your merge,
`getProperty('app.rows', Set)`, `LinkedList`, `Iterable` and `Object` now give
`LinkedHashMap` elements, as on 7.0.x. Each type is a row in
`PropertySourcesConfigSpec`.
--
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]