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


##########
grails-core/src/main/groovy/org/grails/config/NavigableMapPropertySource.groovy:
##########
@@ -42,62 +44,97 @@ import org.springframework.util.StringUtils
 @CompileStatic
 class NavigableMapPropertySource extends MapPropertySource {
 
-    final String[] propertyNames
-    final String[] navigablePropertyNames
-
-    private final Set<String> indexedLists
-    private final Map<String, Object> elementProperties
+    private final Map<String, Object> wrappedElements
 
     NavigableMapPropertySource(String name, NavigableMap source) {
         super(name, source)
-        Set<String> lists = new LinkedHashSet<>()
-        Map<String, Object> elements = new LinkedHashMap<>()
+        Map<String, Object> wrapped = new LinkedHashMap<>()
         for (String key : source.keySet()) {
-            Object value = unwrap(source.get(key))
-            if (value instanceof List && !((List) value).isEmpty()) {
-                lists << key
-                flatten(elements, key, value)
+            Object held = source.get(key)
+            if (held instanceof OriginTrackedValue && isList(unwrap(held))) {
+                // a list held wrapped whole cannot be stepped into or changed 
through the configuration, so its
+                // elements are presented as they were when this source was 
made
+                flatten(wrapped, key, unwrap(held))
             }
         }
-        this.indexedLists = lists
-        this.elementProperties = elements
+        this.wrappedElements = wrapped
+    }
 
+    /**
+     * The names of the values the source holds and of the elements of its 
lists, read from the source on each
+     * call: a block is presented through its entries, which the source holds 
under their own flat keys, a list
+     * through its elements and a map written by a map subscript, such as 
{@code grails.cors.mappings[/api/**]},
+     * through its entries under their bracketed keys, so none of the three is 
a name.
+     */
+    @Override
+    String[] getPropertyNames() {

Review Comment:
   Gone with the split: `NavigableMapPropertySource` is 9.0.x's again, which 
builds `propertyNames` once in its constructor and returns the same array on 
every call, so a miss returns at the identity check in `Cache.tryUpdate` as 
before.
   



##########
grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy:
##########
@@ -190,41 +537,325 @@ 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)
             }
         }
     }
 
+    /**
+     * An entry of a map a subscript wrote merged over the one it meets. A map 
is merged into the map a subscript
+     * wrote that it meets, in place, so every holder of it sees the merge, or 
into a copy of any other map, entry
+     * by entry, so that a property source above another adds to an element as 
Spring Boot binds both and the
+     * configuration holds no map of a property source. A block is merged into 
it by the dotted names of its
+     * entries, as dotted keys are. Anything else is stored as a merge stores 
it; a {@code null} written in dot
+     * notation removes the entry.
+     */
+    private Object mergedElement(Object current, Object value, boolean 
isNestedSet) {
+        boolean currentIsMap = current instanceof Map && !(current instanceof 
NavigableMap)
+        if (!(value instanceof Map)) {
+            return merged(value)
+        }
+        Map held
+        if (currentIsMap && current instanceof HashMap && (current instanceof 
SubscriptMap || !(value instanceof SubscriptMap))) {
+            held = (Map) current
+        } else {
+            held = value instanceof SubscriptMap || current instanceof 
SubscriptMap ? new SubscriptMap() : new LinkedHashMap()
+            if (currentIsMap) {
+                held.putAll((Map) current)
+                if (current instanceof SubscriptMap) {
+                    ((SubscriptMap) 
held).getMarkedKeys().addAll(((SubscriptMap) current).getMarkedKeys())
+                }
+            }
+        }
+        if (value instanceof NavigableMap) {
+            mergeBlockInto(held, '', (NavigableMap) value, isNestedSet)
+            return held
+        }
+        for (Map.Entry entry in ((Map) value).entrySet()) {
+            String key = String.valueOf(entry.key)
+            if (entry.value == null && isNestedSet) {
+                held.remove(key)
+            } else {
+                held.put(key, mergedElement(held.get(key), entry.value, 
isNestedSet))
+            }
+            if (held instanceof SubscriptMap && value instanceof SubscriptMap) 
{
+                if (((SubscriptMap) value).marks(entry.key)) {
+                    ((SubscriptMap) held).mark(key)
+                } else {
+                    ((SubscriptMap) held).unmark(key)
+                }
+            }
+        }
+        held
+    }
+
+    /**
+     * Merges the entries of a block into a map a subscript wrote under the 
names their dotted paths give them, as
+     * dotted keys are written there.
+     */
+    private void mergeBlockInto(Map held, String prefix, NavigableMap block, 
boolean isNestedSet) {
+        for (Map.Entry<String, Object> entry in 
block.getDelegateMap().entrySet()) {
+            String name = prefix + entry.key
+            if (entry.value instanceof NavigableMap) {
+                mergeBlockInto(held, name + '.', (NavigableMap) entry.value, 
isNestedSet)
+            } else {
+                writeBelowSubscriptMap(held, name, entry.value, isNestedSet)
+            }
+        }
+    }
+
+    /** 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('.')) {
+            String[] containerParts = containerName.split(/\./)
+            Object[] below = subscriptMapOnPath(targetMap, containerParts, 
containerParts.length - 1)
+            if (below != null) {
+                // a container below a map a subscript wrote is an entry of 
that map, written in place
+                Object[] slot = slotBelow((Map) below[0], (String) below[1], 
true)
+                Map holder = (Map) slot[0]
+                int end = subscripts.indexOf(']')
+                holder.put(slot[1], setSubscriptValue(holder.get(slot[1]), 
subscripts.substring(1, end), subscripts.substring(end + 1),
+                        merged(sourceValue)))
+                return
+            }
+            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)
+        if (rootMap != null && !containerParent.is(rootMap)) {
+            mergeMapEntry(rootMap, containerPath, container)
+        }
+    }
+
+    /** Marks an indexed key that names no element. */
+    private static final Object ABSENT = new Object()
+
+    /**
+     * The element an indexed key names, such as {@code a.rows[0].name}: read 
through the container this map holds
+     * under the name before the first subscript, then the element each 
subscript and each name after it steps
+     * through, so that the key answers from the list as it is now, however 
the list was written and whatever
+     * was set, added or removed in it since. A name after a subscript is 
matched against the longest key the
+     * element holds, since a merge stores what follows the first subscript of 
an indexed key, {@code tags[0]} or
+     * {@code meta.old}, as one key of the element. {@link #ABSENT} when the 
key has no subscript or names nothing.
+     */
+    private Object elementAt(String key) {
+        Object[] slot = elementSlot(key)
+        if (slot == null) {
+            return ABSENT
+        }
+        Object container = slot[0]
+        String step = (String) slot[1]
+        container instanceof List ? ((List) container).get(step.toInteger()) : 
((Map) container).get(step)
+    }
+
+    /**
+     * The container the last step of an indexed key reads from and that step, 
as {@code [list, index]} or
+     * {@code [map, key]}, reached by stepping through the containers and 
elements before it; null when the key
+     * has no subscript or steps through nothing.
+     */
+    private Object[] elementSlot(String key) {
+        int subscript = subscriptStart(key)
+        if (subscript < 0) {
+            return null
+        }
+        String containerName = key.substring(0, subscript)
+        Object current = delegateMap.get(containerName)
+        if (current == null) {
+            Object atPath = atPath(containerName)
+            current = ABSENT.is(atPath) ? null : atPath
+        }
+        String remaining = key.substring(subscript)
+        while (current != null) {
+            Object next
+            String step
+            String rest
+            if (remaining.startsWith('[')) {
+                int end = remaining.indexOf(']')
+                if (end < 0) {
+                    return null
+                }
+                step = remaining.substring(1, end)
+                rest = remaining.substring(end + 1)
+                next = subscriptOf(current, step)
+            } else if (remaining.startsWith('.') && current instanceof Map) {
+                String name = remaining.substring(1)
+                int split = longestKeyHeld((Map) current, name)
+                if (split < 0) {
+                    return null
+                }
+                step = name.substring(0, split)
+                rest = name.substring(split)
+                next = ((Map) current).get(step)
+            } else {
+                return null
+            }
+            if (ABSENT.is(next)) {
+                return null
+            }
+            if (!rest) {
+                return [current, step] as Object[]
+            }
+            current = next
+            remaining = rest
+        }
+        null
+    }
+
+    /** The element at a subscript, of a list for a numeric index and of a map 
otherwise; {@link #ABSENT} when there is none. */
+    private static Object subscriptOf(Object container, String index) {
+        if (index.isInteger() && !(container instanceof Map)) {
+            if (!(container instanceof List)) {
+                return ABSENT
+            }
+            List list = (List) container
+            int i = index.toInteger()
+            return i >= 0 && i < list.size() ? list.get(i) : ABSENT
+        }
+        container instanceof Map && ((Map) container).containsKey(index) ? 
((Map) container).get(index) : ABSENT
+    }
+
+    /**
+     * The length of the longest prefix of {@code rest} that the map holds as 
a key, ending where the rest ends or
+     * before one of its separators, a dot or a subscript; -1 when it holds 
none.
+     */
+    private static int longestKeyHeld(Map map, String rest) {
+        int split = rest.length()
+        while (split > 0) {
+            if (map.containsKey(rest.substring(0, split))) {
+                return split
+            }
+            split = Math.max(rest.lastIndexOf('.', split - 1), 
rest.lastIndexOf('[', split - 1))
+        }
+        -1
+    }
+
+    /**
+     * The value a merge stores for a scalar, in the nested entry and in the 
flat key alike, and for each element
+     * of a list and each entry of a plain map it stores. A config resolves 
placeholders here, so that a value
+     * such as {@code ${random.uuid}} is resolved once and both 
representations hold the same result. A nested map
+     * stores a value as the root of its configuration does, so a value is 
stored alike whichever map it is
+     * written through.
+     */
+    protected Object mergedValue(Object value) {
+        value
+    }
+
+    /**
+     * The value a merge stores: a scalar as {@link #mergedValue} returns it, 
and a list or a plain map with each
+     * element or entry stored the same way, in place, so that a placeholder 
inside a list element is resolved as
+     * one in an entry of a block is. A container that refuses the write, as a 
list built with
+     * {@code asImmutable()} does, is replaced by a copy that holds the 
results; one whose elements are stored as
+     * they are is left as it is.
+     */
+    private Object merged(Object value) {

Review Comment:
   Gone with the split: a merge stores values as 9.0.x does, without the hook 
that resolved them in place. The separate PR for the flat key that keeps an 
unresolved placeholder will resolve into a copy, and its spec will assert that 
the caller's list and map and the property source's are unchanged.
   



##########
grails-skills/upgrade-guide-9/skills/grails-9-upgrade/SKILL.md:
##########
@@ -438,6 +451,7 @@ A datastore implementation of RxGORM compiled for Grails 8 
or earlier, such as t
 - [ ] No build file declares `grails-async-gpars` or `grails-events-gpars`, 
and no source file imports `groovyx.gpars`
 - [ ] No source file calls `GParsPool.withPool` or `GParsConfig`
 - [ ] No build file sets `grails { indy = true }`, and build logic reads 
`grails.indy` with `getOrElse` or `present`
+- [ ] No code reads a child of a string, list or closure configuration value 
expecting the parent, and none writes into a nested configuration map with 
`put` and reads the value back through `getProperty`

Review Comment:
   Rewritten with the split: the skill now covers only the key past a value 
that is not a map, as the guide does, with one action and one checklist item.
   



##########
grails-doc/src/en/guide/upgrading/upgrading90x.adoc:
##########
@@ -1229,3 +1229,26 @@ See <<asyncPromises,Promises>> and <<events,Events>>.
 
 `grails-async-rxjava3` carries a `null` value as the RxJava 1 factory and 
every other promise factory do: `createPromise()` and `createPromise(Class)` 
give a promise whose `get()` returns `null`, a task that returns `null` 
resolves to `null`, and a `null` value keeps its place in the list that 
`onComplete`, `onError` and `waitAll` deliver.
 `grails-async-rxjava2` does not: there `createPromise()` and `onComplete` over 
a list holding a bound promise of `null` fail with a `NullPointerException`, 
and a task that returns `null` resolves to the `java.lang.Void` class.
+
+==== 20. Configuration Lookups Stop at a Value That Is Not a Map and Read the 
Flat Key First

Review Comment:
   Done with the split. Section 20 is now "A Configuration Key Below a Value 
That Is Not a Map Is Absent": the observable change, with examples of a string, 
a list and a closure, and its required action. The PR no longer adds a what's 
new sentence.
   



##########
grails-bootstrap/src/main/groovy/org/grails/config/NavigableMap.groovy:
##########
@@ -262,27 +957,61 @@ 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. 
The index of a map is its key as
+     * written, a dot in it included, in a block as in any other map.
      */
     private Object setSubscriptValue(Object currentValue, String index, String 
rest, Object sourceValue) {
-        if (index.isNumber()) {
+        if (index ==~ /\d+/) {
             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)
+        }
+        if (currentValue instanceof NavigableMap) {
+            // the entry a read of the key finds: the one held under the key 
as written, or else the one at the
+            // path it names, as a dotted key of application.yml is merged; a 
new entry is held as written
+            NavigableMap block = (NavigableMap) currentValue
+            if (!block.delegateMap.containsKey(index) && 
!ABSENT.is(block.atPath(index))) {

Review Comment:
   The representation this comment was about is gone. Following @jdaugherty's 
review, this PR keeps the nested walk first and holds only the change that 
makes a key past a value that is not a map absent, so `NavigableMap` and 
`NavigableMapPropertySource` are 9.0.x's again. There, `app.routes[/api/**]` 
written after `app.routes.extra` is presented to the environment by its 
bracketed name, and Spring Boot binds the key `/api/**`. `toProperties()` 
exports that name too, beside the dotted `app.routes./api/**` that 9.0.x and 
Grails 8 also export.
   



-- 
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