Repository: geoserver/geoserver
Pull request #8366: [GEOS-11747] GeoServer does not throw JAI runtime exceptions
Author: login: sikeoka
Labels: name: backport 2.26.x
description: Instructs the bot to create a 2.26.x backport PR on merge
Created: 2025-03-04T17:04:31Z | Merged: 2025-03-05T16:47:13Z

[![GEOS-11747](https://badgen.net/badge/JIRA/GEOS-11747/0052CC)](https://osgeo-org.atlassian.net/browse/GEOS-11747) [<img width="16" alt="Powered by Pull Request Badge" src="https://user-images.githubusercontent.com/1393946/111216524-d2bb8e00-85d4-11eb-821b-ed4c00989c02.png">](https://pullrequestbadge.com/?utm_medium=github&utm_source=geoserver&utm_campaign=badge_info)<!-- PR-BADGE: PLEASE DO NOT REMOVE THIS COMMENT -->

<!--Include a few sentences describing the overall goals for this Pull Request-->
This PR updates GeoServer's ImagingListener to throw runtime exceptions that don't come from an OperationRegistry and don't meet the other conditions to hide an exception. It was also moved into a separate method that is called in GeoServerSystemTestSupport so that all GeoServer unit tests now use the GeoServer listener instead of the GeoTools one. The GeoTools listener has the same if statement to throw JAI runtime exceptions so there was different behavior between the behavior in unit tests and when GeoServer was deployed in a server.

This PR is related to https://github.com/geosolutions-it/jai-ext/pull/307
  
<!-- Please help our volunteers reviewing this PR by completing the following items. 
Ask in a comment if you have troubles with any of them. -->

# Checklist

- [x] I have read the [contribution guidelines](https://github.com/geoserver/geoserver/blob/main/CONTRIBUTING.md).
- [x] I have sent a [Contribution Licence Agreement](https://docs.geoserver.org/latest/en/developer/policies/committing.html) (not required for small changes, e.g., fixing typos in documentation).
- [x] First PR targets the `main` branch (backports managed later; ignore for branch specific issues).

For core and extension modules:

- [x] New unit tests have been added covering the changes.
- [ ] [Documentation](https://github.com/geoserver/geoserver/tree/main/doc/en/user/source) has been updated (if change is visible to end users).
- [ ] The [REST API docs](https://github.com/geoserver/geoserver/tree/main/doc/en/api/1.0.0) have been updated (when changing configuration objects or the REST controllers).
- [x] There is an issue in the [GeoServer Jira](https://osgeo-org.atlassian.net/browse/GEOS/summary) (except for changes that do not affect administrators or end users in any way).
- [x] Commit message(s) must be in the form ``[GEOS-XYZWV] Title of the Jira ticket``.
- [x] Bug fixes and small new features are presented as a single commit.
- [x] Each commit has a single objective (if there are multiple commits, each has a separate JIRA ticket describing its goal).

<!--Submitting the PR does not require you to check all items, but by the time it gets merged, they should be either satisfied or not applicable.-->

The PR will be merged when all the build checks are green ([see automated QA checks](https://docs.geoserver.org/latest/en/developer/qa-guide/index.html)), there is a code committer review, and the checklist has been fulfilled.


Diff:
diff --git a/src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java b/src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java
index adcc3912235..0a624526f78 100644
--- a/src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java
+++ b/src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java
@@ -91,38 +91,7 @@ public void contextInitialized(ServletContextEvent sce) {
         LOGGER.config("Logging policy: " + policy);
         GeoTools.init((Hints) null);
 
-        // Custom GeoTools ImagingListener used to ignore common warnings
-        JAI.getDefaultInstance().setImagingListener(new ImagingListener() {
-            final Logger LOGGER = Logging.getLogger("javax.media.jai");
-
-            @Override
-            public boolean errorOccurred(String message, Throwable thrown, Object where, boolean isRetryable)
-                    throws RuntimeException {
-                if (isSerializableRenderedImageFinalization(where, thrown)) {
-                    LOGGER.log(Level.FINEST, message, thrown);
-                } else if (message.contains("Continuing in pure Java mode")) {
-                    LOGGER.log(Level.FINE, message, thrown);
-                } else {
-                    LOGGER.log(Level.INFO, message, thrown);
-                }
-                return false; // we are not trying to recover
-            }
-
-            private boolean isSerializableRenderedImageFinalization(Object where, Throwable t) {
-                if (!(where instanceof SerializableRenderedImage)) {
-                    return false;
-                }
-
-                // check if it's the finalizer
-                StackTraceElement[] elements = t.getStackTrace();
-                for (StackTraceElement element : elements) {
-                    if (element.getMethodName().equals("finalize")
-                            && element.getClassName().endsWith("SerializableRenderedImage")) return true;
-                }
-
-                return false;
-            }
-        });
+        initJAIDefaultInstance();
 
         // setup concurrent operation registry
         JAI jaiDef = JAI.getDefaultInstance();
@@ -220,6 +189,48 @@ private boolean isSerializableRenderedImageFinalization(Object where, Throwable
         Hints.putSystemDefault(Hints.EXECUTOR_SERVICE, executor);
     }
 
+    /** Sets a custom ImagingListener used to ignore common warnings */
+    public static void initJAIDefaultInstance() {
+        JAI jaiDef = JAI.getDefaultInstance();
+        if (!(jaiDef.getImagingListener() instanceof GeoServerImagingListener)) {
+            jaiDef.setImagingListener(new GeoServerImagingListener());
+        }
+    }
+
+    private static final class GeoServerImagingListener implements ImagingListener {
+        private static final Logger LOGGER = Logging.getLogger("javax.media.jai");
+
+        @Override
+        public boolean errorOccurred(String message, Throwable thrown, Object where, boolean isRetryable)
+                throws RuntimeException {
+            if (isSerializableRenderedImageFinalization(where, thrown)) {
+                LOGGER.log(Level.FINEST, message, thrown);
+            } else if (message.contains("Continuing in pure Java mode")) {
+                LOGGER.log(Level.FINE, message, thrown);
+            } else if (thrown instanceof RuntimeException && !(where instanceof OperationRegistry)) {
+                throw (RuntimeException) thrown;
+            } else {
+                LOGGER.log(Level.INFO, message, thrown);
+            }
+            return false; // we are not trying to recover
+        }
+
+        private boolean isSerializableRenderedImageFinalization(Object where, Throwable t) {
+            if (!(where instanceof SerializableRenderedImage)) {
+                return false;
+            }
+
+            // check if it's the finalizer
+            StackTraceElement[] elements = t.getStackTrace();
+            for (StackTraceElement element : elements) {
+                if (element.getMethodName().equals("finalize")
+                        && element.getClassName().endsWith("SerializableRenderedImage")) return true;
+            }
+
+            return false;
+        }
+    }
+
     /**
      * Unregisters providers in the "https://github.com/geosolutions-it/evo-odas/issues/102" for a given category
      * (reader, writer). ImageIO contains a pure java reader and a writer, but also a couple based on native libs (if
diff --git a/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java b/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java
index aca3106a13d..763900d472b 100644
--- a/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java
+++ b/src/main/src/test/java/org/geoserver/test/GeoServerSystemTestSupport.java
@@ -71,6 +71,7 @@
 import org.custommonkey.xmlunit.XMLUnit;
 import org.custommonkey.xmlunit.XpathEngine;
 import org.custommonkey.xmlunit.exceptions.XpathException;
+import org.geoserver.GeoserverInitStartupListener;
 import org.geoserver.catalog.CascadeDeleteVisitor;
 import org.geoserver.catalog.Catalog;
 import org.geoserver.catalog.CoverageInfo;
@@ -335,6 +336,9 @@ protected final void setUp(SystemTestData testData) throws Exception {
             // Allow resolution of XSDs from local file system
             EntityResolverProvider.setEntityResolver(RESOLVE_DISABLED_PROVIDER_DEVMODE);
 
+            // Use GeoServer's JAI ImagingListener
+            GeoserverInitStartupListener.initJAIDefaultInstance();
+
             getSecurityManager().setAuthenticationCache(new TestingAuthenticationCache());
             onSetUp(testData);
         }
diff --git a/src/wms/src/test/java/org/geoserver/wms/map/RenderedImageMapOutputFormatTest.java b/src/wms/src/test/java/org/geoserver/wms/map/RenderedImageMapOutputFormatTest.java
index 67cb6e36788..e7b8ff0c1dd 100644
--- a/src/wms/src/test/java/org/geoserver/wms/map/RenderedImageMapOutputFormatTest.java
+++ b/src/wms/src/test/java/org/geoserver/wms/map/RenderedImageMapOutputFormatTest.java
@@ -1680,6 +1680,29 @@ public void testFaultyStyleDoesntBreak() throws Exception {
         assertNotNull(image);
         assertBlank("testFaultyStyleDoesntBreak", image);
     }
+
+    @Test
+    public void testJAIImagingListenerException() throws Exception {
+        // this style contains a Jiffle script that will cause a JiffleRuntimeException to be thrown and passed to the
+        // JAI ImagingListener set by org.geoserver.GeoserverInitStartupListener
+        String layer = MockData.TASMANIA_DEM.getPrefix() + ":" + MockData.TASMANIA_DEM.getLocalPart();
+        MockHttpServletResponse response = getAsServletResponse(
+                "wms?request=GetMap&format=image/png&width=256&height=256&srs=EPSG:4326&bbox=-180,-90,180,90&layers="
+                        + layer
+                        + "&sld_body=<StyledLayerDescriptor><NamedLayer><Name>"
+                        + layer
+                        + "</Name><UserStyle><FeatureTypeStyle><Transformation><Function name=\"ras:Jiffle\">"
+                        + "<Function name=\"parameter\"><Literal>coverage</Literal></Function>"
+                        + "<Function name=\"parameter\"><Literal>script</Literal><Literal>dest=src[1000,1000];</Literal>"
+                        + "</Function></Function></Transformation><Rule><RasterSymbolizer/></Rule>"
+                        + "</FeatureTypeStyle></UserStyle></NamedLayer></StyledLayerDescriptor>");
+        assertEquals("text/xml;charset=UTF-8", response.getContentType());
+        String content = response.getContentAsString();
+        assertThat(content, containsString("ServiceExceptionReport"));
+        assertThat(content, containsString("jiffle.runtime.JiffleRuntimeException"));
+        assertThat(content, containsString(" is outside bounds of image: src"));
+    }
+
     /**
      * This dummy producer adds no functionality to DefaultRasterMapOutputFormat, just implements a void
      * formatImageOutputStream to have a concrete class over which test that DefaultRasterMapOutputFormat correctly


Reviews:
user: login: jodygarnett
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T07:59:01Z
user: login: jodygarnett
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T08:02:18Z
user: login: sikeoka
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T15:47:37Z

Review comments:
diff_hunk: @@ -220,6 +189,49 @@ private boolean isSerializableRenderedImageFinalization(Object where, Throwable
         Hints.putSystemDefault(Hints.EXECUTOR_SERVICE, executor);
     }
 
+    /** Initializes the default JAI ImagingListener */
+    public static void initJAIDefaultInstance() {
+        JAI jaiDef = JAI.getDefaultInstance();
+        // Custom ImagingListener used to ignore common warnings
+        if (!(jaiDef.getImagingListener() instanceof GeoServerImagingListener)) {
+            jaiDef.setImagingListener(new GeoServerImagingListener());
+        }
+    }
+
path: src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java
user: login: jodygarnett
body: ```suggestion
   /**
    * Custom GeoTools ImagingListener used to ignore common warnings.
    */
```
created_at: 2025-03-05T07:57:38Z
author_association: MEMBER
diff_hunk: @@ -220,6 +189,49 @@ private boolean isSerializableRenderedImageFinalization(Object where, Throwable
         Hints.putSystemDefault(Hints.EXECUTOR_SERVICE, executor);
     }
 
+    /** Initializes the default JAI ImagingListener */
+    public static void initJAIDefaultInstance() {
+        JAI jaiDef = JAI.getDefaultInstance();
+        // Custom ImagingListener used to ignore common warnings
+        if (!(jaiDef.getImagingListener() instanceof GeoServerImagingListener)) {
+            jaiDef.setImagingListener(new GeoServerImagingListener());
+        }
+    }
+
+    private static final class GeoServerImagingListener implements ImagingListener {
+        private static final Logger LOGGER = Logging.getLogger("org.geoserver.jai");
path: src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java
user: login: jodygarnett
body: I am not sure how I feel about changing the logger in this manner, if you wish to do so please note the change in the update instrutions also.

aside: Looking ahead to GeoServer 3 we will be changing to ImageN shortly, can you think of a different logger that would not need to get renamed?
created_at: 2025-03-05T08:02:04Z
author_association: MEMBER
diff_hunk: @@ -220,6 +189,49 @@ private boolean isSerializableRenderedImageFinalization(Object where, Throwable
         Hints.putSystemDefault(Hints.EXECUTOR_SERVICE, executor);
     }
 
+    /** Initializes the default JAI ImagingListener */
+    public static void initJAIDefaultInstance() {
+        JAI jaiDef = JAI.getDefaultInstance();
+        // Custom ImagingListener used to ignore common warnings
+        if (!(jaiDef.getImagingListener() instanceof GeoServerImagingListener)) {
+            jaiDef.setImagingListener(new GeoServerImagingListener());
+        }
+    }
+
+    private static final class GeoServerImagingListener implements ImagingListener {
+        private static final Logger LOGGER = Logging.getLogger("org.geoserver.jai");
path: src/main/src/main/java/org/geoserver/GeoserverInitStartupListener.java
user: login: sikeoka
body: I removed the logger change. The main focus of the PR should just be to throw the exceptions.
created_at: 2025-03-05T15:47:37Z
author_association: COLLABORATOR

PR comments:
user: login: jodygarnett
created_at: 2025-03-05T16:47:30Z
body: Do you need a back port?
author_association: MEMBER
user: login: sikeoka
created_at: 2025-03-05T16:55:22Z
body: Yes
author_association: COLLABORATOR