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

enorman pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-bundleresource-impl.git


The following commit(s) were added to refs/heads/master by this push:
     new 3887fd2  SLING-12979 migrate to Sling API 3.x and Jakarta Servlet (#7)
3887fd2 is described below

commit 3887fd2d58827666e4d713d5ee8e2356e6de5bbc
Author: Eric Norman <[email protected]>
AuthorDate: Tue Nov 4 14:10:03 2025 -0800

    SLING-12979 migrate to Sling API 3.x and Jakarta Servlet (#7)
---
 pom.xml                                            |  20 +-
 .../impl/BundleResourceWebConsolePlugin.java       |  67 ++++---
 .../impl/BundleResourceWebConsolePluginTest.java   | 212 +++++++++++++++++++++
 3 files changed, 261 insertions(+), 38 deletions(-)

diff --git a/pom.xml b/pom.xml
index 844ae84..a161919 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
     </parent>
 
     <artifactId>org.apache.sling.bundleresource.impl</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>3.0.0-SNAPSHOT</version>
 
     <name>Apache Sling Bundle Resource Provider</name>
     <description>Provides a ResourceProvider implementation supporting bundle
@@ -41,18 +41,21 @@
     </scm>
 
     <properties>
-        <sling.java.version>11</sling.java.version>
+        <sling.java.version>17</sling.java.version>
+        <slf4j.version>2.0.17</slf4j.version>
         
<project.build.outputTimestamp>2025-03-05T17:41:02Z</project.build.outputTimestamp>
     </properties>
     <dependencies>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
-            <groupId>javax.servlet</groupId>
-            <artifactId>javax.servlet-api</artifactId>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <version>6.1.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -64,7 +67,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.api</artifactId>
-            <version>2.25.4</version>
+            <version>3.0.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -99,31 +102,30 @@
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
+            <version>${slf4j.version}</version>
             <scope>provided</scope>
         </dependency>
         <!-- Testing -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
-            <version>5.10.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-engine</artifactId>
-            <version>5.10.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>5.5.0</version>
+            <version>5.20.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.johnzon</groupId>
             <artifactId>johnzon-core</artifactId>
-            <version>2.0.1</version>
+            <version>2.0.2</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git 
a/src/main/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePlugin.java
 
b/src/main/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePlugin.java
index ecd8073..5a4ed20 100644
--- 
a/src/main/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePlugin.java
+++ 
b/src/main/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePlugin.java
@@ -18,19 +18,19 @@
  */
 package org.apache.sling.bundleresource.impl;
 
-import javax.servlet.Servlet;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.Dictionary;
 import java.util.Hashtable;
 import java.util.List;
-
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -45,31 +45,34 @@ class BundleResourceWebConsolePlugin extends HttpServlet {
 
     private static final String LABEL = "bundleresources";
 
-    private volatile ServiceRegistration<Servlet> serviceRegistration;
+    // runtime-only holders — transient so servlet serialization won't try to 
persist them
+    private final transient AtomicReference<ServiceRegistration<Servlet>> 
serviceRegistration = new AtomicReference<>();
 
     @SuppressWarnings("rawtypes")
-    private volatile ServiceTracker<ResourceProvider, ResourceProvider> 
providerTracker;
+    private final transient AtomicReference<ServiceTracker<ResourceProvider, 
ResourceProvider>> providerTracker =
+            new AtomicReference<>();
 
-    private final List<BundleResourceProvider> provider = new ArrayList<>();
+    // thread-safe list so ServiceTracker callbacks can add/remove while doGet 
iterates
+    private final transient List<BundleResourceProvider> provider = new 
CopyOnWriteArrayList<>();
 
     // --------- setup and shutdown
 
-    private static BundleResourceWebConsolePlugin INSTANCE;
+    private static BundleResourceWebConsolePlugin instance;
 
     static void initPlugin(BundleContext context) {
-        if (INSTANCE == null) {
+        if (instance == null) {
             BundleResourceWebConsolePlugin tmp = new 
BundleResourceWebConsolePlugin();
             tmp.activate(context);
-            INSTANCE = tmp;
+            instance = tmp;
         }
     }
 
     static void destroyPlugin() {
-        if (INSTANCE != null) {
+        if (instance != null) {
             try {
-                INSTANCE.deactivate();
+                instance.deactivate();
             } finally {
-                INSTANCE = null;
+                instance = null;
             }
         }
     }
@@ -145,7 +148,7 @@ class BundleResourceWebConsolePlugin extends HttpServlet {
 
     @SuppressWarnings("rawtypes")
     public void activate(BundleContext context) {
-        providerTracker =
+        ServiceTracker<ResourceProvider, ResourceProvider> tracker =
                 new ServiceTracker<ResourceProvider, ResourceProvider>(
                         context, ResourceProvider.class.getName(), null) {
 
@@ -154,8 +157,8 @@ class BundleResourceWebConsolePlugin extends HttpServlet {
                         ResourceProvider service = null;
                         if 
(reference.getProperty(BundleResourceProvider.PROP_BUNDLE) != null) {
                             service = super.addingService(reference);
-                            if (service instanceof BundleResourceProvider) {
-                                provider.add((BundleResourceProvider) service);
+                            if (service instanceof BundleResourceProvider 
brpService) {
+                                provider.add(brpService);
                             }
                         }
                         return service;
@@ -170,27 +173,33 @@ class BundleResourceWebConsolePlugin extends HttpServlet {
                         super.removedService(reference, service);
                     }
                 };
-        providerTracker.open();
+        providerTracker.set(tracker);
+        tracker.open();
 
-        Dictionary<String, Object> props = new Hashtable<>();
+        Dictionary<String, Object> props = new Hashtable<>(); // NOSONAR
         props.put(Constants.SERVICE_DESCRIPTION, "Web Console Plugin for 
Bundle Resource Providers");
         props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
         props.put("felix.webconsole.label", LABEL);
         props.put("felix.webconsole.title", "Bundle Resource Provider");
         props.put("felix.webconsole.category", "Sling");
 
-        serviceRegistration = context.registerService(Servlet.class, this, 
props);
+        serviceRegistration.set(context.registerService(Servlet.class, this, 
props));
     }
 
     public void deactivate() {
-        if (serviceRegistration != null) {
-            serviceRegistration.unregister();
-            serviceRegistration = null;
+        ServiceRegistration<Servlet> sr = serviceRegistration.getAndSet(null);
+        if (sr != null) {
+            try {
+                sr.unregister();
+            } catch (IllegalStateException ise) {
+                // ignore if already unregistered
+            }
         }
 
-        if (providerTracker != null) {
-            providerTracker.close();
-            providerTracker = null;
+        @SuppressWarnings("rawtypes")
+        ServiceTracker<ResourceProvider, ResourceProvider> t = 
providerTracker.getAndSet(null);
+        if (t != null) {
+            t.close();
         }
     }
 
diff --git 
a/src/test/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePluginTest.java
 
b/src/test/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePluginTest.java
new file mode 100644
index 0000000..8c9dd23
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/bundleresource/impl/BundleResourceWebConsolePluginTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.sling.bundleresource.impl;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Basic unit tests for BundleResourceWebConsolePlugin to exercise activation, 
simple doGet rendering,
+ * and deactivation/unregister behavior.
+ *
+ * - Calls initPlugin(BundleContext) with a mocked BundleContext that returns 
a mocked ServiceRegistration.
+ * - Reflectively obtains the created plugin instance and invokes doGet to 
capture the generated HTML.
+ * - Calls destroyPlugin() and verifies the ServiceRegistration.unregister() 
was invoked.
+ */
+class BundleResourceWebConsolePluginTest {
+
+    @Test
+    void testActivateDoGetDeactivate() throws Exception {
+        testPlugin((ctx, reg, plugin) -> {
+            // calling init again should do no additional work
+            BundleResourceWebConsolePlugin.initPlugin(ctx);
+            BundleResourceWebConsolePlugin plugin2 = getInstanceField();
+            assertSame(plugin2, plugin);
+
+            mockBundleResourceProvider(ctx, plugin);
+
+            // Prepare mocks for servlet invocation and capture output
+            HttpServletRequest req = mock(HttpServletRequest.class);
+            HttpServletResponse resp = mock(HttpServletResponse.class);
+
+            StringWriter sw = new StringWriter();
+            PrintWriter pw = new PrintWriter(sw);
+            when(resp.getWriter()).thenReturn(pw);
+
+            // Invoke doGet which should render an HTML table (even with no 
providers)
+            plugin.doGet(req, resp);
+            pw.flush();
+            String output = sw.toString();
+
+            // Assert: basic expected content is present
+            assertTrue(output.contains("Bundle Resource Provider"), "Output 
should contain the webconsole title");
+            assertTrue(output.contains("<table"), "Output should contain a 
table element");
+
+            // Clean up: destroy plugin (should unregister the service)
+            BundleResourceWebConsolePlugin.destroyPlugin();
+            // calling a second time should do no additional work
+            BundleResourceWebConsolePlugin.destroyPlugin();
+
+            // Verify unregister was called on the registration
+            verify(reg, times(1)).unregister();
+        });
+    }
+
+    @Test
+    void testDeactivateTwice() throws Exception {
+        testPlugin((ctx, reg, plugin) -> {
+            plugin.deactivate();
+            // Verify unregister was called on the registration
+            verify(reg, times(1)).unregister();
+
+            // call deactivate again should do no addiional work
+            Mockito.reset((Object) reg);
+            plugin.deactivate();
+            // Verify unregister was called on the registration
+            verify(reg, times(0)).unregister();
+        });
+    }
+
+    @Test
+    void testDeactivateWithServiceRegAlreadyUnregistered() throws Exception {
+        testPlugin((ctx, reg, plugin) -> {
+            
Mockito.doThrow(IllegalStateException.class).when(reg).unregister();
+            plugin.deactivate();
+            // Verify unregister was called on the registration
+            verify(reg, times(1)).unregister();
+        });
+    }
+
+    /**
+     * Simulate a BundleResourceProvider service being registered and added to 
the service tracker
+     */
+    private void mockBundleResourceProvider(BundleContext ctx, 
BundleResourceWebConsolePlugin plugin)
+            throws NoSuchFieldException, SecurityException, 
IllegalArgumentException, IllegalAccessException {
+        @SuppressWarnings("rawtypes")
+        final ServiceTracker<ResourceProvider, ResourceProvider> 
providerTracker = getProviderTrackerField(plugin);
+        assertNotNull(providerTracker);
+        final BundleResourceProvider mockResourceProvider1 = 
Mockito.mock(BundleResourceProvider.class);
+        BundleResourceCache cache = Mockito.mock(BundleResourceCache.class);
+        Bundle mockBundle = Mockito.mock(Bundle.class);
+        Dictionary<String, Object> headers = new 
Hashtable<>(Map.of(Constants.BUNDLE_NAME, "test.bundle1"));
+        Mockito.doReturn(headers).when(mockBundle).getHeaders();
+        Mockito.doReturn(mockBundle).when(cache).getBundle();
+        
Mockito.doReturn(cache).when(mockResourceProvider1).getBundleResourceCache();
+
+        PathMapping pathMapping = Mockito.mock(PathMapping.class);
+        
Mockito.doReturn(pathMapping).when(mockResourceProvider1).getMappedPath();
+
+        @SuppressWarnings({"unchecked", "rawtypes"})
+        ServiceReference<ResourceProvider> ref1 = mock(ServiceReference.class);
+        
Mockito.doReturn(1L).when(ref1).getProperty(BundleResourceProvider.PROP_BUNDLE);
+        Mockito.doReturn(mockResourceProvider1).when(ctx).getService(ref1);
+        providerTracker.addingService(ref1);
+    }
+
+    /**
+     * Reflectively clear the private static instance so we can start from 
scratch
+     */
+    private void clearInstanceField()
+            throws NoSuchFieldException, SecurityException, 
IllegalArgumentException, IllegalAccessException {
+        Field instanceField = 
BundleResourceWebConsolePlugin.class.getDeclaredField("instance");
+        instanceField.setAccessible(true);
+        instanceField.set(null, null);
+    }
+
+    /**
+     * Reflectively fetch the private static instance so we can call doGet on 
it
+     */
+    private BundleResourceWebConsolePlugin getInstanceField()
+            throws NoSuchFieldException, SecurityException, 
IllegalArgumentException, IllegalAccessException {
+        Field instanceField = 
BundleResourceWebConsolePlugin.class.getDeclaredField("instance");
+        instanceField.setAccessible(true);
+        return (BundleResourceWebConsolePlugin) instanceField.get(null);
+    }
+
+    /**
+     * Reflectively fetch the private static instance so we can call doGet on 
it
+     */
+    @SuppressWarnings("rawtypes")
+    private ServiceTracker<ResourceProvider, ResourceProvider> 
getProviderTrackerField(
+            BundleResourceWebConsolePlugin plugin)
+            throws NoSuchFieldException, SecurityException, 
IllegalArgumentException, IllegalAccessException {
+        Field instanceField = 
BundleResourceWebConsolePlugin.class.getDeclaredField("providerTracker");
+        instanceField.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        AtomicReference<ServiceTracker<ResourceProvider, ResourceProvider>> 
ref =
+                (AtomicReference<ServiceTracker<ResourceProvider, 
ResourceProvider>>) instanceField.get(plugin);
+        return ref.get();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected void testPlugin(PluginWorker worker) throws Exception {
+        BundleContext ctx = mock(BundleContext.class);
+        ServiceRegistration<Servlet> reg = mock(ServiceRegistration.class);
+
+        // registerService should return our mock registration
+        when(ctx.registerService(eq(Servlet.class), any(Servlet.class), 
any(Dictionary.class)))
+                .thenReturn(reg);
+
+        // make sure the is nothing left in this field
+        clearInstanceField();
+
+        // Act: initialize plugin (this creates and activates the plugin 
instance)
+        BundleResourceWebConsolePlugin.initPlugin(ctx);
+
+        // Reflectively fetch the private static instance so we can call doGet 
on it
+        BundleResourceWebConsolePlugin plugin = getInstanceField();
+        // ensure we have a plugin instance
+        assertNotNull(plugin, "Plugin instance should have been created");
+
+        worker.doWork(ctx, reg, plugin);
+    }
+
+    public static interface PluginWorker {
+        public void doWork(BundleContext ctx, ServiceRegistration<Servlet> 
reg, BundleResourceWebConsolePlugin plugin)
+                throws Exception;
+    }
+}

Reply via email to