codeconsole commented on code in PR #16732:
URL: https://github.com/apache/grails-core/pull/16732#discussion_r4373769110


##########
grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy:
##########
@@ -264,27 +688,33 @@ class NavigableMap implements Map<String, Object>, 
Cloneable {
             if (submap instanceof Map) {
                 return navigateMap((Map<String, Object>) submap, path.tail())
             }
-            return submap
+            return null
         }
     }
 
     /**
      * Sets a value in a list (numeric index) or map (other index) at the 
given subscript, creating
      * the container when the current value is not one. The text following the 
subscript is either
      * empty, further subscripts such as {@code [1]} for a list nested in a 
list, or a {@code .name}
-     * remainder stored as a key of a map element.
+     * remainder stored as a key of a map element. A container that already 
holds the object is left
+     * as it is, so a merge of the same entries again, as a config merges its 
property sources again,
+     * writes nothing into it. A container that refuses the write, such as a 
list {@code application.groovy}
+     * built with {@code asImmutable()}, is replaced by a copy that holds it.
      */
     private Object setSubscriptValue(Object currentValue, String index, String 
rest, Object sourceValue) {
         if (index.isNumber()) {
             List list = currentValue instanceof List ? (List) currentValue : []
             int i = index.toInteger()
-            fill(list, i, null)
-            list.set(i, subscriptElementValue(list.get(i), rest, sourceValue))
-            return list
+            boolean held = i < list.size()
+            Object element = held ? list.get(i) : null
+            Object newElement = subscriptElementValue(element, rest, 
sourceValue)
+            if (held && element.is(newElement)) {
+                return list
+            }
+            return writeElement(list, i, newElement)
         }
-        Map nestedMap = currentValue instanceof Map ? (Map) currentValue : [:]
-        nestedMap.put(index, subscriptElementValue(nestedMap.get(index), rest, 
sourceValue))
-        return nestedMap
+        Map map = currentValue instanceof Map ? (Map) currentValue : [:]

Review Comment:
   Fixed in 6d2cdedad9. A map subscript written into a block went through the 
block's `put`, which reads a dotted key as a path, so `app.routes[/v1.0/**]` 
over a map from `application.groovy` wrote `/v1` -> `0/**` beside the entry and 
left the entry on `old`. The index is now written as the key of the block as 
written, in both representations, as it is into any other map. 
`PropertySourcesConfigSpec` runs your sequence through a 
`NavigableMapPropertySource` and asserts the property becomes `new`; 
`NavigableMapSpec` asserts it for merge, put, subscript assignment, dot 
notation and a write through the nested map.
   
   987884c616 covers the other way a block can hold that key: as the path `/v1` 
-> `0/**`, which is how a dotted key of `application.yml` is stored. There the 
bracketed write updates the entry the bracketed read finds, not a second entry 
under the same flat key.
   
   The converse, a dotted key of `application.yml` over a key with a dot that a 
map in `application.groovy` holds, leaves the two as separate entries in Grails 
8 and on the base of this PR alike. A fix needs a rule for which form wins 
across all three merge paths, so it is #16766 rather than part of this PR.
   



##########
grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy:
##########
@@ -192,41 +357,239 @@ class NavigableMap implements Map<String, Object>, 
Cloneable {
                 else {
                     subMap = new NavigableMap(targetMap.getRootConfig(), 
newPathList.asImmutable())
                     if (currentValue instanceof Map) {
-                        subMap.putAll((Map) currentValue)
+                        subMap.delegateMap.putAll((Map) currentValue)
                     }
                 }
                 String newPath = path ? "${path}.${sourceKey}" : sourceKey
-                mergeMaps(rootMap, newPath , subMap, (Map) sourceValue, 
parseFlatKeys)
+                // the entries of a map written in dot notation are written as 
dot notation writes them, so a value
+                // replaces a block of the map it is merged into and a null 
removes it
+                mergeMaps(rootMap, newPath, subMap, (Map) sourceValue, 
parseFlatKeys, isNestedSet)
                 newValue = subMap
             } else {
-                newValue = sourceValue
+                newValue = merged(sourceValue)
             }
             if (isNestedSet && newValue == null) {
-                if (path) {
-
-                    def subMap = rootMap.get(path)
-                    if (subMap instanceof Map) {
-                        subMap.remove(sourceKey)
-                    }
-                    def keysToRemove = rootMap.keySet().findAll() { String key 
->
-                        key.startsWith("${path}.")
-                    }
-                    for (key in keysToRemove) {
-                        rootMap.remove(key)
-                    }
+                // the flat key of the removed property and those below it go 
with it; the flat keys of its siblings stay
+                String removedKey = path ? "${path}.${sourceKey}".toString() : 
sourceKey
+                removeKeysBelow(rootMap, removedKey)
+                if (rootMap != null) {
+                    rootMap.delegateMap.remove(removedKey)
                 }
-                targetMap.remove(sourceKey)
+                targetMap.delegateMap.remove(sourceKey)
             } else {
-                if (path) {
-                    rootMap.put("${path}.${sourceKey}".toString(), newValue)
+                if (isNestedSet && !(newValue instanceof NavigableMap) && 
targetMap.delegateMap.get(sourceKey) instanceof NavigableMap) {
+                    // a value written over a block replaces it, so no flat 
key of its entries is read any more; a merge
+                    // keeps them, as the environment reads a key a 
lower-precedence property source sets
+                    removeKeysBelow(rootMap, path ? 
"${path}.${sourceKey}".toString() : sourceKey)
+                }
+                if (path && rootMap != null) {
+                    mergeMapEntry(rootMap, "${path}.${sourceKey}".toString(), 
newValue)
                 }
                 mergeMapEntry(targetMap, sourceKey, newValue)
             }
         }
     }
 
+    /** Removes the flat keys below a flat key, those of the entries of the 
block it named. */
+    private void removeKeysBelow(NavigableMap rootMap, String flatKey) {
+        if (rootMap == null) {
+            return
+        }
+        String prefix = flatKey + '.'
+        List<String> keysBelow = rootMap.keySet().findAll { String key -> 
key.startsWith(prefix) } as List<String>
+        for (String key in keysBelow) {
+            rootMap.delegateMap.remove(key)
+        }
+    }
+
+    /** The index of the first subscript of a key such as {@code 
rows[0].name}, or -1 when the key has none. */
+    private static int subscriptStart(String key) {
+        int start = key.indexOf('[')
+        start > -1 && key.indexOf(']', start) > start ? start : -1
+    }
+
+    /**
+     * Merges a key with a subscript, such as {@code rows[0].name}, {@code 
groups[0][1]} or {@code mappings[/api/**]}.
+     * The container named before the first subscript, a list for a numeric 
index and a map otherwise, is held by
+     * the nested map that owns it and, under its flat key, by the root, as 
one object in both places. Nothing is
+     * held per element: {@code a.rows[0]} and {@code a.rows[0].name} are read 
through the container, so they
+     * answer from the list as it is when they are read. The dots of the 
container's name are a path only when
+     * the merge parses flat keys, as for any other key.
+     */
+    private void mergeIndexedEntry(NavigableMap rootMap, String path, 
NavigableMap targetMap, String containerName,
+                                   String subscripts, Object sourceValue, 
boolean parseFlatKeys, boolean isNestedSet) {
+        NavigableMap containerParent = targetMap
+        String containerKey = containerName
+        if (parseFlatKeys && containerName.contains('.')) {
+            List<String> nameParts = containerName.split(/\./) as List<String>
+            containerParent = targetMap.navigateSubMap(nameParts[0..-2], true, 
rootMap)
+            containerKey = nameParts[-1]
+        }
+        String containerPath = path ? "${path}.${containerName}".toString() : 
containerName
+        int subscriptEnd = subscripts.indexOf(']')
+        Object previousContainer = containerParent.get(containerKey)
+        Object value = merged(sourceValue)
+        Object container = setSubscriptValue(previousContainer, 
subscripts.substring(1, subscriptEnd),
+                subscripts.substring(subscriptEnd + 1), value)
+        if (isNestedSet && previousContainer instanceof NavigableMap && 
!container.is(previousContainer)) {
+            // a list written over a block replaces it, as a value written 
over a block does
+            removeKeysBelow(rootMap, containerPath)
+        }
+        mergeMapEntry(containerParent, containerKey, container)

Review Comment:
   Fixed in 9552c45ed2, e88bf72988 and d5e63b1050. `toProperties()` and 
`toFlatConfig()` name the entries of a map a subscript wrote by their bracketed 
keys, `app.routes[/api/**]`, and write a map entry through its values only, as 
`NavigableMapPropertySource` presents the map to Spring. e88bf72988 covers 
`application.yml`: `PropertySourcesConfig` turned such a map into a block when 
it merged the file's source, so the YAML form of `grails.cors.mappings` still 
exported `./api/**`. d5e63b1050 drops the `{allowedOrigins[0]=...}` String that 
an entry holding a map was also exported as, which Spring Boot bound in place 
of the map. `YamlPropertySourceLoaderSpec` publishes the export to an 
environment, as a `doWithConfig` block does, and asserts it binds to the same 
map as the file.
   
   Checking the export and the property source against Spring's `Binder` turned 
up more of the same kind, each with a failing-first test:
   
   - b472e32b5a: a key in brackets after another subscript, `app.rows[0][/a.b]` 
or `app.grid[/x][/y.z]`, was presented to Spring as `app.rows[0]./a.b` and 
bound as `a: [b: ...]`. It is now presented and exported in brackets, and a 
name after a subscript, `app.rows[0].name`, stays dotted.
   - 56cc9b19aa: two property sources that each write an entry into one element 
of such a map both land in it, as Spring Boot binds them, and a write into the 
config no longer reaches the property source's maps.
   - c457d71af9: a dotted key, a map, or a profile's block written over a map a 
subscript wrote, such as `app.routes.extra` from a profile over 
`app.routes['[/api/**]']`, replaced the map and dropped its entries. It now 
writes into the map, which stays bracketed. A name is a key of the map, or of a 
map the map holds under a key the name starts with, as Spring Boot binds a map 
of strings or of maps. So `grails.mail.props.mail.smtp.auth` writes the entry 
`'[mail.smtp.auth]'` wrote, and `...mail.smtp.auth.mechanisms` writes a second 
one beside it.
   - 2466252e39 and e56f324f7e: the environment answers each key of such a map 
by the name the file wrote it under, dotted or bracketed. A whole-number key, 
or one holding `]`, is named in dot notation, since in brackets it would name a 
list index or end early.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to