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

jamesfredley pushed a commit to branch feat/url-mapping-precompute-seed
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 97b5cc9ec5549c604e4888e1b665aa70dbd807e0
Author: James Fredley <[email protected]>
AuthorDate: Fri Jul 10 12:57:45 2026 -0400

    Seed URL mappings index with runtime fallback
    
    Add UrlMappingsIndexProperties and load path with fallback to runtime 
UrlMappingsHolder behavior.
    
    Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]
---
 grails-doc/src/en/guide/toc.yml                    |  1 +
 .../en/guide/upgrading/urlMappingsPrecompute.adoc  |  7 ++
 .../web/mapping/DefaultUrlMappingsHolder.java      |  9 +++
 .../web/mapping/UrlMappingsIndexProperties.java    | 80 ++++++++++++++++++++++
 .../mapping/UrlMappingsIndexPropertiesSpec.groovy  | 35 ++++++++++
 5 files changed, 132 insertions(+)

diff --git a/grails-doc/src/en/guide/toc.yml b/grails-doc/src/en/guide/toc.yml
index b0e4a5c3ff..c3bb13effa 100644
--- a/grails-doc/src/en/guide/toc.yml
+++ b/grails-doc/src/en/guide/toc.yml
@@ -37,6 +37,7 @@ gettingStarted:
 upgrading:
   title: Upgrading from the previous versions
   upgrading80x: Upgrading from Grails 7 to Grails 8
+  urlMappingsPrecompute: URL mapping precomputation seed
   upgrading72x: Upgrading from Grails 7.1 to Grails 7.2
   upgrading71x: Upgrading from Grails 7.0 to Grails 7.1
   upgrading70x: Upgrading from Grails 6 to Grails 7.0
diff --git a/grails-doc/src/en/guide/upgrading/urlMappingsPrecompute.adoc 
b/grails-doc/src/en/guide/upgrading/urlMappingsPrecompute.adoc
new file mode 100644
index 0000000000..33c4462e87
--- /dev/null
+++ b/grails-doc/src/en/guide/upgrading/urlMappingsPrecompute.adoc
@@ -0,0 +1,7 @@
+=== URL mapping precomputation seed
+
+Grails 8.1 starts reserving `META-INF/grails/url-mappings-index.properties` as 
the build-time URL mappings index location.
+The runtime `DefaultUrlMappingsHolder` now detects that descriptor and keeps 
the existing runtime mapping evaluation path as the fallback when no descriptor 
is packaged.
+
+This is a first compatibility slice only.
+Applications do not need to generate the descriptor yet, and existing 
`UrlMappings.groovy` behavior remains authoritative until a later release wires 
a complete build-time index generator.
diff --git 
a/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultUrlMappingsHolder.java
 
b/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultUrlMappingsHolder.java
index 6d0ff5f437..ad912f2f81 100644
--- 
a/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultUrlMappingsHolder.java
+++ 
b/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultUrlMappingsHolder.java
@@ -101,6 +101,7 @@ public class DefaultUrlMappingsHolder implements 
UrlMappings {
     private final Set<String> DEFAULT_ACTION_PARAMS = 
CollectionUtils.newSet(UrlMapping.ACTION);
     private final PathMatcher pathMatcher = new AntPathMatcher();
     private final AtomicInteger initCounter = new AtomicInteger();
+    private final UrlMappingsIndexProperties precomputedIndexProperties;
 
     public DefaultUrlMappingsHolder(List<UrlMapping> mappings) {
         this(mappings, null, false);
@@ -113,6 +114,7 @@ public class DefaultUrlMappingsHolder implements 
UrlMappings {
     public DefaultUrlMappingsHolder(List<UrlMapping> mappings, List 
excludePatterns, boolean doNotCallInit) {
         urlMappings = mappings;
         this.excludePatterns = excludePatterns;
+        this.precomputedIndexProperties = 
UrlMappingsIndexProperties.load(DefaultUrlMappingsHolder.class.getClassLoader());
         if (!doNotCallInit) {
             initialize();
         }
@@ -132,6 +134,9 @@ public class DefaultUrlMappingsHolder implements 
UrlMappings {
 
     public void initialize() {
         sortMappings();
+        if (precomputedIndexProperties.isPresent() && LOG.isDebugEnabled()) {
+            LOG.debug("Discovered " + UrlMappingsIndexProperties.LOCATION + "; 
runtime URL mapping fallback remains active");
+        }
 
         cachedMatches = Caffeine.newBuilder()
             .maximumSize(maxWeightedCacheCapacity)
@@ -233,6 +238,10 @@ public class DefaultUrlMappingsHolder implements 
UrlMappings {
         return excludePatterns;
     }
 
+    public UrlMappingsIndexProperties getPrecomputedIndexProperties() {
+        return precomputedIndexProperties;
+    }
+
     public UrlCreator getReverseMapping(final String controller, final String 
action, Map params) {
         return getReverseMapping(controller, action, null, null, params);
     }
diff --git 
a/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/UrlMappingsIndexProperties.java
 
b/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/UrlMappingsIndexProperties.java
new file mode 100644
index 0000000000..318be2ca39
--- /dev/null
+++ 
b/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/UrlMappingsIndexProperties.java
@@ -0,0 +1,80 @@
+/*
+ *  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 org.grails.web.mapping;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Properties;
+
+/**
+ * Descriptor for a future build-time URL mappings index.
+ *
+ * @since 8.1
+ */
+public final class UrlMappingsIndexProperties {
+
+    public static final String LOCATION = 
"META-INF/grails/url-mappings-index.properties";
+
+    private static final UrlMappingsIndexProperties EMPTY = new 
UrlMappingsIndexProperties(false, new Properties());
+
+    private final boolean present;
+    private final Properties properties;
+
+    private UrlMappingsIndexProperties(boolean present, Properties properties) 
{
+        this.present = present;
+        this.properties = properties;
+    }
+
+    public static UrlMappingsIndexProperties load(ClassLoader classLoader) {
+        ClassLoader loader = classLoader == null ? 
Thread.currentThread().getContextClassLoader() : classLoader;
+        if (loader == null) {
+            return EMPTY;
+        }
+        try (InputStream inputStream = loader.getResourceAsStream(LOCATION)) {
+            if (inputStream == null) {
+                return EMPTY;
+            }
+            Properties properties = new Properties();
+            properties.load(inputStream);
+            return new UrlMappingsIndexProperties(true, properties);
+        }
+        catch (IOException e) {
+            throw new IllegalStateException("Unable to load " + LOCATION, e);
+        }
+    }
+
+    public boolean isPresent() {
+        return present;
+    }
+
+    public Properties asProperties() {
+        Properties copy = new Properties();
+        copy.putAll(properties);
+        return copy;
+    }
+
+    public String getProperty(String name) {
+        return properties.getProperty(name);
+    }
+
+    public Iterable<String> propertyNames() {
+        return present ? properties.stringPropertyNames() : 
Collections.emptySet();
+    }
+}
diff --git 
a/grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/UrlMappingsIndexPropertiesSpec.groovy
 
b/grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/UrlMappingsIndexPropertiesSpec.groovy
new file mode 100644
index 0000000000..6ce2015112
--- /dev/null
+++ 
b/grails-web-url-mappings/src/test/groovy/org/grails/web/mapping/UrlMappingsIndexPropertiesSpec.groovy
@@ -0,0 +1,35 @@
+/*
+ *  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 org.grails.web.mapping
+
+import grails.web.mapping.UrlMapping
+import spock.lang.Specification
+
+class UrlMappingsIndexPropertiesSpec extends Specification {
+
+    void 'missing build-time URL mapping index keeps runtime fallback 
active'() {
+        when:
+        DefaultUrlMappingsHolder holder = new DefaultUrlMappingsHolder([] as 
List<UrlMapping>)
+
+        then:
+        !holder.precomputedIndexProperties.present
+        holder.urlMappings.length == 0
+        holder.matchAll('/books').length == 0
+    }
+}

Reply via email to