You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/10/10 16:49:12 UTC

(grails-core) 01/03: fix(web-core): keep GrailsEnvironment a POJO so dynamic getProperty resolves config

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 9.0.x-java-to-groovy
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit d13cd4c03ec82d92334249d2f29964e824f3724e
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sat Oct 10 11:42:45 2026 -0500

    fix(web-core): keep GrailsEnvironment a POJO so dynamic getProperty resolves config
    
    Converted to Groovy, GrailsEnvironment became a GroovyObject and gained the
    GroovyObject#getProperty(String) default method next to the inherited Spring
    PropertyResolver#getProperty(String). The two do not override each other at
    the JVM level (String vs Object return type, and the Groovy compiler emits no
    bridge), and Groovy's metaclass keeps the interface method when a superclass
    method with the same parameters is indexed later, so every dynamic
    environment.getProperty('a.b') call ran the Groovy property lookup and threw
    MissingPropertyException instead of resolving the Spring property sources
    (external-configuration ExternalConfigSpec/MergedConfigSpec in CI).
    
    @POJO restores the Java class' shape: no GroovyObject, so the Spring method is
    the only getProperty(String) again. The new spec makes the lookups through
    plain call sites, which is the path that failed; a call written inside a Spock
    condition is dispatched differently and did not reproduce the regression.
    
    Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
---
 .../context/support/GrailsEnvironment.groovy       |  7 ++
 .../context/support/GrailsEnvironmentSpec.groovy   | 98 ++++++++++++++++++++++
 2 files changed, 105 insertions(+)

diff --git a/grails-web-core/src/main/groovy/grails/web/servlet/context/support/GrailsEnvironment.groovy b/grails-web-core/src/main/groovy/grails/web/servlet/context/support/GrailsEnvironment.groovy
index 553450fc25..0e03d108f5 100644
--- a/grails-web-core/src/main/groovy/grails/web/servlet/context/support/GrailsEnvironment.groovy
+++ b/grails-web-core/src/main/groovy/grails/web/servlet/context/support/GrailsEnvironment.groovy
@@ -19,6 +19,7 @@
 package grails.web.servlet.context.support
 
 import groovy.transform.CompileStatic
+import groovy.transform.stc.POJO
 import org.springframework.core.env.PropertiesPropertySource
 import org.springframework.core.env.PropertySource
 import org.springframework.util.StringUtils
@@ -30,9 +31,15 @@ import grails.util.Environment
 /**
  * Bridges Grails' existing environment API with the new Spring 3.1 environment profiles API.
  *
+ * <p>The class is a {@link POJO}: Spring's {@code PropertyResolver#getProperty(String)} returns
+ * {@code String}, so it does not override {@code GroovyObject#getProperty(String)} (which returns
+ * {@code Object}), and a dynamic {@code environment.getProperty('a.b')} call would otherwise reach
+ * the Groovy property lookup instead of the Spring property resolution.</p>
+ *
  * @author Graeme Rocher
  * @since 2.0
  */
+@POJO
 @CompileStatic
 class GrailsEnvironment extends StandardServletEnvironment {
 
diff --git a/grails-web-core/src/test/groovy/grails/web/servlet/context/support/GrailsEnvironmentSpec.groovy b/grails-web-core/src/test/groovy/grails/web/servlet/context/support/GrailsEnvironmentSpec.groovy
new file mode 100644
index 0000000000..0913d20e12
--- /dev/null
+++ b/grails-web-core/src/test/groovy/grails/web/servlet/context/support/GrailsEnvironmentSpec.groovy
@@ -0,0 +1,98 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package grails.web.servlet.context.support
+
+import grails.core.DefaultGrailsApplication
+import grails.core.GrailsApplication
+import org.grails.config.PropertySourcesConfig
+import org.springframework.core.env.ConfigurableEnvironment
+import org.springframework.core.env.MapPropertySource
+import spock.lang.Specification
+
+/**
+ * The environment is used from dynamic Groovy (Spock specs, scripts, the external configuration
+ * plugin), where {@code environment.getProperty('a.b')} must be the Spring
+ * {@link org.springframework.core.env.PropertyResolver} lookup and never the Groovy property
+ * lookup, which would throw a {@link MissingPropertyException} for a dotted config key.
+ *
+ * <p>The lookups are plain dynamic method calls assigned in {@code when:} blocks on purpose: a call
+ * written inside a Spock condition is dispatched differently and does not exercise the same
+ * Groovy call-site path as application code or a helper method.</p>
+ */
+class GrailsEnvironmentSpec extends Specification {
+
+    private final GrailsApplication grailsApplication = new DefaultGrailsApplication()
+
+    ConfigurableEnvironment environment
+
+    void setup() {
+        grailsApplication.config = new PropertySourcesConfig(['yml.config': 'yml-expected-value', 'nested.config.value': 'nested-value'])
+        environment = new GrailsEnvironment(grailsApplication)
+    }
+
+    void "a dotted config property is resolved through the Spring property resolver"() {
+        when:
+        String top = lookup('yml.config')
+        String nested = lookup('nested.config.value')
+        String missing = lookup('missing.config')
+        String defaulted = environment.getProperty('missing.config', 'fallback')
+
+        then:
+        top == 'yml-expected-value'
+        nested == 'nested-value'
+        missing == null
+        defaulted == 'fallback'
+    }
+
+    void "a property source added at runtime takes precedence over the application config"() {
+        given:
+        environment.propertySources.addFirst(new MapPropertySource('added', ['yml.config': 'overridden', 'added.only': 'added-value']))
+
+        when:
+        String overridden = lookup('yml.config')
+        String added = lookup('added.only')
+        String nested = lookup('nested.config.value')
+
+        then:
+        overridden == 'overridden'
+        added == 'added-value'
+        nested == 'nested-value'
+    }
+
+    void "the application config is exposed as a named property source"() {
+        when:
+        def configSource = environment.propertySources.get('grailsApplication')
+        def value = configSource.getProperty('yml.config')
+
+        then:
+        configSource != null
+        value == 'yml-expected-value'
+        environment.propertySources.get('systemProperties') != null
+    }
+
+    void "the current Grails environment name is an active profile"() {
+        expect:
+        environment.activeProfiles.contains(grails.util.Environment.current.name)
+    }
+
+    private String lookup(String key) {
+        environment.getProperty(key)
+    }
+
+}