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


##########
grails-core/src/main/groovy/org/grails/config/NavigableMapConfig.java:
##########
@@ -55,6 +58,7 @@ public abstract class NavigableMapConfig implements Config {
     protected static final Logger LOG = 
LoggerFactory.getLogger(NavigableMapConfig.class);
     protected ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
     protected ConfigurableConversionService conversionService = new 
DefaultConversionService();
+    private final SystemEnvironmentIndex systemEnvironment = new 
SystemEnvironmentIndex(System.getenv());

Review Comment:
   Done in dc3e5d19e8. The index is a `volatile` field that a `protected void 
refreshSystemEnvironment()` on `NavigableMapConfig` re-captures, and 
`PropertySourcesConfig.refresh()` calls it before it merges the property 
sources again, so the raw name and the dotted spelling agree after a refresh. 
Pinned by `a variable installed after the config exists is seen once the config 
is refreshed` in `SystemEnvironmentConfigSpec`: the variable is installed after 
construction, the config still answers `from-yml`, and after `refresh()` it 
answers `from-env`. The 8.1 upgrade guide names 
`grailsApplication.config.refresh()` as the way for a test that installs 
variables after the context exists.
   
   One thing to flag: `refresh()` is on `PropertySourcesConfig`, not on the 
`Config` interface, so from a test it is a dynamic call. The guide says so 
(e9fc57f940), and that `refresh()` merges the sources again and needs calling 
again once the variables are removed, since the config keeps the environment it 
last read. Putting `refresh()` on `Config` would be an API addition, so I left 
it out; say if you want it there.
   



##########
grails-core/src/main/groovy/org/grails/config/NavigableMapConfig.java:
##########
@@ -219,7 +223,7 @@ public <T> T getProperty(String key, Class<T> targetType, T 
defaultValue, List<T
 
     @Override
     public <T> T getProperty(String key, Class<T> targetType, T defaultValue) {
-        Object value = findInSystemEnvironment(key);
+        Object value = systemEnvironment.get(key);
         if (value == null) {
             value = getValueWithDotNotatedKeySupport(configMap, key);

Review Comment:
   A raw sub-map `put` turns out not to be the deciding case: flat-first also 
changes answers for configs built from supported writes alone. 
`merge(['server.url.port': 'y', 'server.url': 'x'])` is what layering a 
higher-precedence scalar over a lower-precedence nested block produces at 
startup, since `initializeFromPropertySources` merges the sources lowest 
precedence first and `merge` leaves the stale descendant flat key behind; it 
answers `server.url.port` with `x` today and `y` flat-first. `put('a', 
'scalar'); put('a.b', 1)` answers `a.b` with `scalar` today and `1` flat-first. 
Those are the two flat-key states you asked to move to 9.0.x, so this PR keeps 
walk-first, and the description now says which raw write wins as it ships: a 
raw sub-map `put` is visible to `getProperty`, while a root `put` or `setAt` 
over a merged key changes only `config['a.b']`.
   
   Flat-first goes into the 9.0.x PR with the absent-past-scalar change. There 
the walk already answers those two cases with the flat value, so flat-first 
only adds which raw single-representation write wins, and one upgrade note 
covers both. With the pre-filter in, the gap on this harness is 104 ns 
walk-first against the 55 ns you measured flat-first.
   



##########
grails-core/src/main/groovy/org/grails/config/NavigableMapConfig.java:
##########
@@ -404,27 +360,115 @@ private Object 
getValueWithDotNotatedKeySupport(NavigableMap configMap, String k
             return null;
         }
 
-        List<String> keys = convertTokensIntoArrayList(new 
StringTokenizer(key, "."));
-        if (keys.size() == 0) {
-            return null;
-        }
-
         Object value = null;
-        for (int i = 0; i < keys.size(); i++) {
-            if (i == 0) {
-                value = configMap.get(keys.get(i));
-            } else if (value instanceof Map) {
-                value = ((Map) value).get(keys.get(i));
+        boolean first = true;
+        int length = key.length();
+        int start = 0;
+        while (start < length) {
+            int end = key.indexOf('.', start);
+            if (end < 0) {
+                end = length;
+            }
+            if (end > start) {
+                String segment = key.substring(start, end);
+                if (first) {
+                    value = configMap.get(segment);
+                    first = false;
+                } else if (value instanceof Map) {
+                    value = ((Map<?, ?>) value).get(segment);
+                } else {
+                    // The key names a child of a scalar, which does not exist 
whatever the scalar is.
+                    return null;
+                }
             }
+            start = end + 1;
         }
         return value;
     }
 
-    private List<String> convertTokensIntoArrayList(StringTokenizer st) {
-        List<String> elements = new ArrayList<>();
-        while (st.hasMoreTokens()) {
-            elements.add(st.nextToken());
+    /**
+     * The process environment as it stood when this config was created, 
indexed so that a key no
+     * variable can match is answered with one hash lookup.
+     * <p>
+     * A key matches a variable in any of the spellings Spring's {@code 
SystemEnvironmentPropertySource}
+     * accepts: as written, with dots replaced by underscores, with hyphens 
replaced by underscores,
+     * with both replaced, and the upper-cased form of each. Every one of 
those spellings has the same
+     * canonical name, so a key whose canonical name belongs to no variable 
cannot match and is
+     * rejected without probing. Only a key whose canonical name is present 
runs the spelling-by-spelling
+     * resolution, which keeps the precedence between spellings as it was.
+     * <p>
+     * The environment of a running JVM cannot change, so one snapshot per 
config instance is exact.
+     * The memory it takes is proportional to the number of environment 
variables, not to the size of
+     * the configuration or to the number of keys looked up.
+     */
+    private static final class SystemEnvironmentIndex {
+
+        /**
+         * {@code System.getenv(String)} matches a variable name 
case-insensitively on Windows, where
+         * the operating system does, and exactly everywhere else. The 
snapshot keeps that contract.
+         */
+        private static final boolean IS_WINDOWS = 
System.getProperty("os.name", "").startsWith("Windows");
+
+        private final Map<String, String> variables;
+        private final Set<String> canonicalNames;
+
+        SystemEnvironmentIndex(Map<String, String> environment) {
+            this.variables = IS_WINDOWS ? new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER) : new HashMap<>();
+            this.variables.putAll(environment);
+            this.canonicalNames = new HashSet<>();
+            for (String name : variables.keySet()) {
+                canonicalNames.add(canonicalName(name));
+            }
+        }
+
+        String get(String key) {
+            if (key == null || !canonicalNames.contains(canonicalName(key))) {
+                return null;
+            }
+            return variables.get(resolvePropertyName(key));
+        }
+
+        private static String canonicalName(String name) {
+            return name.toUpperCase(Locale.ROOT).replace('.', 
'_').replace('-', '_');

Review Comment:
   Done in c58e746a18. `mayMatch` folds each character of the key to its 
canonical form (`a`-`z` to upper case, `.` and `-` to `_`) and accumulates 
`String.hashCode` over them, then looks the hash up in the sorted `int[]` of 
the variables' canonical-name hashes; only a hit builds the canonical name for 
the exact set check before the spelling-by-spelling resolution. A character 
above 127 takes the exact check directly, so `straße` still matches `STRASSE`. 
Both collections are sized up front with `HashMap.newHashMap` and 
`HashSet.newHashSet`. On the description's harness here: 141 → 104 ns per call 
(60 variables).
   
   The `ß` fallback has its own feature in 3c61a8142f: `straße.x` matches 
`STRASSE_X`, which the per-character fold alone cannot do.
   



##########
grails-core/src/test/groovy/org/grails/config/PropertySourcesConfigSpec.groovy:
##########
@@ -116,4 +116,33 @@ class PropertySourcesConfigSpec extends Specification {
         then: 'the second value is not merged with the first value'
         cfg.foo.bar == ['two': '2']
     }
+
+    void 'a dotted key walks plain nested maps and skips empty segments'() {
+        given: "a plain map value that was put without flattened keys"
+        def config = new PropertySourcesConfig()
+        config.put('plain', [nested: [value: 'found']])
+
+        expect: "each segment of the key is resolved against the map it 
reaches"
+        config.getProperty('plain.nested.value') == 'found'
+        config.getProperty('plain.nested', Map) == [value: 'found']
+        config.getProperty('plain.missing.value') == null
+
+        and: "empty segments are ignored as they always were"
+        config.getProperty('plain..nested.value') == 'found'
+        config.getProperty('.plain.nested.value.') == 'found'
+        config.getProperty('...') == null
+    }
+
+    void 'a dotted key that runs past a scalar value is absent'() {
+        given: "a scalar value"
+        def config = new PropertySourcesConfig([server: [url: 
'http://localhost']])
+
+        expect: "the scalar itself resolves"
+        config.getProperty('server.url') == 'http://localhost'
+
+        and: "a key that names a child of the scalar is absent rather than the 
scalar"
+        config.getProperty('server.url.port') == null

Review Comment:
   Reverted in 1e79f8f3fd, as a revert so the reviewed commits stay intact. The 
`indexOf` walk stays and leaves the value untouched when a segment lands on a 
non-Map, as the `StringTokenizer` walk did, and the feature went with it. The 
9.0.x PR will carry the absent-past-scalar change with the List and closure 
cases, the flat-key states, `navigateMap` and `CodeGenConfig`, flat-first (see 
the other thread) and an upgrade note.
   
   The restored answer is pinned in 66e9bb2abe (`plain.nested.value.deeper` 
answers `found` and `containsProperty` is true), so the 9.0.x PR flips that 
assertion deliberately rather than silently.
   



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