You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/09/13 03:45:34 UTC

(groovy) 01/01: GROOVY-12404: GroovyScriptEngine resource loader leaks URLConnections and ignores a custom ResourceConnector

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

asf-gitbox-commits pushed a commit to branch GROOVY-12404
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit d38a1c289cce37d1697972d2ecad446abb40a686
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Sep 13 12:35:22 2026 +0900

    GROOVY-12404: GroovyScriptEngine resource loader leaks URLConnections and ignores a custom ResourceConnector
---
 src/main/java/groovy/util/GroovyScriptEngine.java  | 45 ++++++-----
 .../groovy/util/GroovyScriptEngineTest.groovy      | 90 ++++++++++++++++++++++
 2 files changed, 116 insertions(+), 19 deletions(-)

diff --git a/src/main/java/groovy/util/GroovyScriptEngine.java b/src/main/java/groovy/util/GroovyScriptEngine.java
index 7b1ae85a0f..a1f3787659 100644
--- a/src/main/java/groovy/util/GroovyScriptEngine.java
+++ b/src/main/java/groovy/util/GroovyScriptEngine.java
@@ -145,6 +145,7 @@ public class GroovyScriptEngine implements ResourceConnector {
          */
         public ScriptClassLoader(GroovyClassLoader loader) {
             super(loader);
+            setResLoader();
         }
 
         /**
@@ -161,14 +162,16 @@ public class GroovyScriptEngine implements ResourceConnector {
         private void setResLoader() {
             final GroovyResourceLoader rl = getResourceLoader();
             setResourceLoader(className -> {
-                String filename;
                 for (String extension : getConfig().getScriptExtensions()) {
-                    filename = className.replace('.', File.separatorChar) + "." + extension;
+                    String filename = className.replace('.', '/') + "." + extension;
+                    URLConnection dependentScriptConn = null;
                     try {
-                        URLConnection dependentScriptConn = rc.getResourceConnection(filename);
+                        dependentScriptConn = rc.getResourceConnection(filename);
                         return dependentScriptConn.getURL();
                     } catch (ResourceException e) {
-                        //TODO: maybe do something here?
+                        // Missing this extension; try the next one, then the parent loader.
+                    } finally {
+                        forceClose(dependentScriptConn);
                     }
                 }
                 return rl.loadGroovySource(className);
@@ -189,7 +192,12 @@ public class GroovyScriptEngine implements ResourceConnector {
             for (String depSourcePath : cache.get(".")) {
                 try {
                     cache.get(depSourcePath);
-                    cu.addSource(getResourceConnection(depSourcePath).getURL());
+                    URLConnection conn = rc.getResourceConnection(depSourcePath);
+                    try {
+                        cu.addSource(conn.getURL());
+                    } finally {
+                        forceClose(conn);
+                    }
                 } catch (ResourceException e) {
                     /* ignore */
                 }
@@ -216,24 +224,23 @@ public class GroovyScriptEngine implements ResourceConnector {
                         try {
                             String finalName = name + "." + ext;
                             URLConnection conn = rc.getResourceConnection(finalName);
-                            URL url = conn.getURL();
-                            String path = url.toExternalForm();
-                            ScriptCacheEntry entry = scriptCache.get(path);
-                            Class clazz = null;
-                            if (entry != null) clazz = entry.scriptClass;
-                            if (GroovyScriptEngine.this.isSourceNewer(entry)) {
-                                try {
+                            try {
+                                URL url = conn.getURL();
+                                String path = url.toExternalForm();
+                                ScriptCacheEntry entry = scriptCache.get(path);
+                                Class clazz = null;
+                                if (entry != null) clazz = entry.scriptClass;
+                                if (GroovyScriptEngine.this.isSourceNewer(entry)) {
                                     SourceUnit su = compilationUnit.addSource(url);
                                     return new LookupResult(su, null);
-                                } finally {
-                                    forceClose(conn);
                                 }
-                            } else {
                                 precompiledEntries.put(origName, path);
-                            }
-                            if (clazz != null) {
-                                ClassNode cn = ClassHelper.make(clazz);
-                                return new LookupResult(null, cn);
+                                if (clazz != null) {
+                                    ClassNode cn = ClassHelper.make(clazz);
+                                    return new LookupResult(null, cn);
+                                }
+                            } finally {
+                                forceClose(conn);
                             }
                         } catch (ResourceException re) {
                             // skip
diff --git a/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy b/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy
index aa15d50170..fea754230b 100644
--- a/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy
+++ b/src/test/groovy/groovy/util/GroovyScriptEngineTest.groovy
@@ -86,4 +86,94 @@ final class GroovyScriptEngineTest {
         assert counts['Script1'] == 1
         assert counts['Foo'] == 1
     }
+
+    @Test
+    void resourceLoaderClosesConnectorConnection() {
+        File scriptFile = new File(temporaryFolder, 'Foo.groovy')
+        scriptFile.text = 'class Foo {}'
+
+        TrackingURLConnection tracking = null
+        ResourceConnector rc = { String name ->
+            File f = new File(temporaryFolder, name)
+            if (!f.exists()) throw new ResourceException("missing $name")
+            tracking = new TrackingURLConnection(f)
+            tracking
+        }
+
+        GroovyScriptEngine engine = new GroovyScriptEngine(rc)
+        engine.config.scriptExtensions = new LinkedHashSet(['groovy'])
+
+        URL url = engine.groovyClassLoader.resourceLoader.loadGroovySource('Foo')
+        assert url != null
+        assert tracking != null
+        assert tracking.inputStreamOpened : 'URLConnection obtained only for its URL must still be closed'
+    }
+
+    @Test
+    void resourceLoaderUsesCustomConnectorWhenParentIsGroovyClassLoader() {
+        new File(temporaryFolder, 'Helper.groovy').text = 'class Helper { def ping() { "pong" } }'
+
+        ResourceConnector rc = { String name ->
+            File f = new File(temporaryFolder, name)
+            if (!f.exists()) throw new ResourceException("missing $name")
+            f.toURI().toURL().openConnection()
+        }
+
+        GroovyScriptEngine engine = new GroovyScriptEngine(rc, new GroovyClassLoader())
+        Class helper = engine.groovyClassLoader.loadClass('Helper')
+        assert helper.newInstance().ping() == 'pong'
+    }
+
+    @Test
+    void resourceLoaderTriesNextExtensionWhenFirstIsMissing() {
+        new File(temporaryFolder, 'Helper.gy').text = 'class Helper { def ping() { "pong" } }'
+
+        GroovyScriptEngine engine = new GroovyScriptEngine([temporaryFolder.toURI().toURL()] as URL[])
+        engine.config.scriptExtensions = new LinkedHashSet(['groovy', 'gy'])
+
+        Class helper = engine.groovyClassLoader.loadClass('Helper')
+        assert helper.newInstance().ping() == 'pong'
+    }
+
+    @Test
+    void resourceLoaderRequestsForwardSlashPathsForPackagedClasses() {
+        File pkg = new File(temporaryFolder, 'com/example')
+        pkg.mkdirs()
+        new File(pkg, 'Helper.groovy').text = 'package com.example; class Helper { def ping() { "pong" } }'
+
+        def requested = []
+        ResourceConnector rc = { String name ->
+            requested << name
+            File f = new File(temporaryFolder, name)
+            if (!f.exists()) throw new ResourceException("missing $name")
+            f.toURI().toURL().openConnection()
+        }
+
+        GroovyScriptEngine engine = new GroovyScriptEngine(rc)
+        engine.config.scriptExtensions = new LinkedHashSet(['groovy'])
+
+        Class helper = engine.groovyClassLoader.loadClass('com.example.Helper')
+        assert helper.newInstance().ping() == 'pong'
+        assert requested.contains('com/example/Helper.groovy')
+    }
+
+    static final class TrackingURLConnection extends URLConnection {
+        boolean inputStreamOpened
+        private final File file
+
+        TrackingURLConnection(File file) {
+            super(file.toURI().toURL())
+            this.file = file
+        }
+
+        @Override
+        void connect() {
+        }
+
+        @Override
+        InputStream getInputStream() {
+            inputStreamOpened = true
+            file.newInputStream()
+        }
+    }
 }