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

jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
     new 033286e15e GH-2741: Clean the OSGi bundle cache on JDK version change 
(#2899)
033286e15e is described below

commit 033286e15e0dfe45cb4e239f5459e45ed31ad0e5
Author: JB Onofré <[email protected]>
AuthorDate: Mon Sep 14 15:19:30 2026 +0200

    GH-2741: Clean the OSGi bundle cache on JDK version change (#2899)
    
    The bundle cache under data/cache can persist a bundle wiring that was
    resolved against a different JDK. After a JDK major version
    upgrade/downgrade, reusing that stale wiring can silently leave core
    bundles (e.g. Configuration Admin, File Install) without expected
    wires, so filesystem/config changes stop being detected with no error
    reported.
    
    Karaf now records the JDK specification version used on the previous
    start in data/jdk_version. On startup, if the recorded version differs
    from the current one, the bundle cache is cleaned before the framework
    is created, forcing a full re-resolution. This can be disabled with
    -Dkaraf.clean.cache.on.jdk.change=false.
---
 .../org/apache/karaf/main/ConfigProperties.java    |  10 ++
 .../java/org/apache/karaf/main/util/Utils.java     |  38 +++++++
 .../java/org/apache/karaf/main/util/UtilsTest.java | 117 +++++++++++++++++++++
 3 files changed, 165 insertions(+)

diff --git a/main/src/main/java/org/apache/karaf/main/ConfigProperties.java 
b/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
index cb186d79c2..895c950356 100644
--- a/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
+++ b/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
@@ -179,6 +179,12 @@ public class ConfigProperties {
      */
     private static final String PROPERTY_USE_LOCK = "karaf.lock";
 
+    /**
+     * When true (default), the OSGi bundle cache is automatically cleaned if 
a JDK version
+     * change is detected since the last start of this instance. See {@link 
Utils#cleanCacheOnJdkChange}.
+     */
+    private static final String PROPERTY_CLEAN_CACHE_ON_JDK_CHANGE = 
"karaf.clean.cache.on.jdk.change";
+
     File karafHome;
     File karafBase;
     File karafData;
@@ -304,6 +310,10 @@ public class ConfigProperties {
             props.setProperty(Constants.FRAMEWORK_STORAGE, 
storage.getAbsolutePath());
         }
 
+        if 
(Boolean.parseBoolean(System.getProperty(PROPERTY_CLEAN_CACHE_ON_JDK_CHANGE, 
"true"))) {
+            Utils.cleanCacheOnJdkChange(karafData, new 
File(props.getProperty(Constants.FRAMEWORK_STORAGE)));
+        }
+
         if (shutdownCommand == null || shutdownCommand.isEmpty()) {
             try {
                 shutdownCommand = UUID.randomUUID().toString();
diff --git a/main/src/main/java/org/apache/karaf/main/util/Utils.java 
b/main/src/main/java/org/apache/karaf/main/util/Utils.java
index f7003fe294..0e0dc94f26 100644
--- a/main/src/main/java/org/apache/karaf/main/util/Utils.java
+++ b/main/src/main/java/org/apache/karaf/main/util/Utils.java
@@ -24,10 +24,48 @@ import java.io.IOException;
 import java.net.JarURLConnection;
 import java.net.URI;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.StringTokenizer;
 
 public class Utils {
 
+    /**
+     * Name of the marker file, stored in the Karaf data directory, recording 
the
+     * specification version of the JDK that was used for the last start of 
the instance.
+     */
+    public static final String JDK_VERSION_MARKER_FILE = "jdk_version";
+
+    /**
+     * Detects a JDK (specification) version change since the last start of 
this instance and,
+     * when detected, cleans the OSGi bundle cache before the framework is 
created.
+     * <p>
+     * The bundle cache can persist a bundle wiring that was resolved against 
a different JDK
+     * (different major version exposes a different set of packages to the 
system bundle). Reusing
+     * such a stale wiring after a JDK upgrade/downgrade can silently leave 
core bundles (e.g.
+     * Configuration Admin, File Install) without expected wires, without any 
error being reported.
+     *
+     * @param karafData the Karaf data directory, where the marker file is 
stored
+     * @param cacheDirectory the OSGi framework bundle cache directory
+     * @throws IOException if the marker file can't be read/written, or the 
cache can't be cleaned
+     */
+    public static void cleanCacheOnJdkChange(File karafData, File 
cacheDirectory) throws IOException {
+        String currentVersion = 
System.getProperty("java.specification.version");
+        File marker = new File(karafData, JDK_VERSION_MARKER_FILE);
+        String previousVersion = null;
+        if (marker.exists()) {
+            previousVersion = new String(Files.readAllBytes(marker.toPath()), 
StandardCharsets.UTF_8).trim();
+        }
+        if (previousVersion != null && !previousVersion.isEmpty() && 
!previousVersion.equals(currentVersion)) {
+            System.err.println("WARN: JDK version change detected since the 
last start (" + previousVersion + " -> " + currentVersion
+                    + "). Cleaning the OSGi bundle cache (" + cacheDirectory + 
") to avoid reusing a bundle wiring resolved against a different JDK.");
+            deleteDirectory(cacheDirectory);
+            cacheDirectory.mkdirs();
+        }
+        karafData.mkdirs();
+        Files.write(marker.toPath(), 
currentVersion.getBytes(StandardCharsets.UTF_8));
+    }
+
     public static File getKarafHome(Class<?> mainClass, String 
karafHomeProperty, String karafHomeEnv) throws IOException {
         File rc = null;
 
diff --git a/main/src/test/java/org/apache/karaf/main/util/UtilsTest.java 
b/main/src/test/java/org/apache/karaf/main/util/UtilsTest.java
new file mode 100644
index 0000000000..c459cec6d1
--- /dev/null
+++ b/main/src/test/java/org/apache/karaf/main/util/UtilsTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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
+ *
+ *   http://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.apache.karaf.main.util;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class UtilsTest {
+
+    @Rule
+    public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+    @Test
+    public void firstStartCreatesMarkerAndKeepsCache() throws Exception {
+        File data = temporaryFolder.newFolder("data");
+        File cache = new File(data, "cache");
+        Files.createDirectories(cache.toPath());
+        Path bundleFile = cache.toPath().resolve("bundle0");
+        Files.write(bundleFile, new byte[0]);
+
+        String originalVersion = 
System.getProperty("java.specification.version");
+        try {
+            System.setProperty("java.specification.version", "17");
+            Utils.cleanCacheOnJdkChange(data, cache);
+
+            assertTrue("cache content must be preserved on first start", 
Files.exists(bundleFile));
+            File marker = new File(data, Utils.JDK_VERSION_MARKER_FILE);
+            assertTrue(marker.exists());
+            assertEquals("17", new String(Files.readAllBytes(marker.toPath()), 
StandardCharsets.UTF_8).trim());
+        } finally {
+            restore(originalVersion);
+        }
+    }
+
+    @Test
+    public void sameJdkVersionKeepsCache() throws Exception {
+        File data = temporaryFolder.newFolder("data");
+        File cache = new File(data, "cache");
+        Files.createDirectories(cache.toPath());
+        Path bundleFile = cache.toPath().resolve("bundle0");
+        Files.write(bundleFile, new byte[0]);
+
+        String originalVersion = 
System.getProperty("java.specification.version");
+        try {
+            System.setProperty("java.specification.version", "17");
+            Utils.cleanCacheOnJdkChange(data, cache);
+            // started again with the same JDK version
+            Utils.cleanCacheOnJdkChange(data, cache);
+
+            assertTrue("cache content must be preserved across restarts with 
the same JDK", Files.exists(bundleFile));
+        } finally {
+            restore(originalVersion);
+        }
+    }
+
+    @Test
+    public void jdkVersionChangeCleansCache() throws Exception {
+        File data = temporaryFolder.newFolder("data");
+        File cache = new File(data, "cache");
+        Files.createDirectories(cache.toPath());
+        Path bundleFile = cache.toPath().resolve("bundle0");
+        Files.write(bundleFile, new byte[0]);
+
+        String originalVersion = 
System.getProperty("java.specification.version");
+        try {
+            System.setProperty("java.specification.version", "11");
+            Utils.cleanCacheOnJdkChange(data, cache);
+            assertTrue(Files.exists(bundleFile));
+
+            // restart under a different major JDK version
+            System.setProperty("java.specification.version", "17");
+            Utils.cleanCacheOnJdkChange(data, cache);
+
+            assertFalse("stale cache from the previous JDK must be cleaned", 
Files.exists(bundleFile));
+            assertTrue("cache directory must be recreated", cache.exists());
+            File marker = new File(data, Utils.JDK_VERSION_MARKER_FILE);
+            assertEquals("17", new String(Files.readAllBytes(marker.toPath()), 
StandardCharsets.UTF_8).trim());
+        } finally {
+            restore(originalVersion);
+        }
+    }
+
+    private void restore(String originalVersion) {
+        if (originalVersion == null) {
+            System.clearProperty("java.specification.version");
+        } else {
+            System.setProperty("java.specification.version", originalVersion);
+        }
+    }
+
+}

Reply via email to