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

cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git

commit 1dabc9ee5ba27a074b5a81f97ae7f2ef06802410
Author: Carsten Ziegeler <cziege...@apache.org>
AuthorDate: Sat Sep 9 11:26:17 2023 +0200

    FELIX-6638 : Migrate WebConsole to Jakarta Servlet API
---
 webconsole/pom.xml                                 |  12 +-
 .../apache/felix/webconsole/WebConsoleUtil.java    |  45 +-
 .../org/apache/felix/webconsole/internal/Util.java |  73 ++
 .../internal/configuration/ConfigManager.java      |   3 -
 .../webconsole/internal/core/BundlesServlet.java   |   3 -
 .../webconsole/internal/core/ServicesServlet.java  |   5 +-
 .../internal/filter/FilteringResponseWrapper.java  |   2 -
 .../internal/legacy/LegacyServicesTracker.java     |  79 +-
 .../{ => legacy}/WebConsolePluginAdapter.java      |  57 +-
 .../internal/misc/ConfigurationRender.java         |  11 +-
 .../internal/servlet/AbstractInternalPlugin.java   | 984 ---------------------
 .../servlet/AbstractOsgiManagerPlugin.java         |   3 -
 .../internal/servlet/AbstractPluginAdapter.java    | 651 ++++++++++++++
 .../servlet/BasicWebConsoleSecurityProvider.java   |   4 +-
 .../internal/servlet/ConfigurationUtil.java        |   1 +
 ...vletAdapter.java => EnhancedPluginAdapter.java} |  85 +-
 .../webconsole/internal/servlet/OsgiManager.java   |  71 +-
 .../internal/servlet/OsgiManagerHttpContext.java   |   1 +
 .../webconsole/internal/servlet/Password.java      | 134 +--
 .../felix/webconsole/internal/servlet/Plugin.java  |  30 +-
 .../webconsole/internal/servlet/PluginHolder.java  |  14 +-
 .../internal/servlet/SimplePluginAdapter.java      | 159 ++++
 .../BasicWebConsoleSecurityProviderTest.java       |  48 +
 .../servlet/OsgiManagerHttpContextTest.java        |  88 --
 .../internal/servlet/OsgiManagerTest.java          |   8 +-
 25 files changed, 1108 insertions(+), 1463 deletions(-)

diff --git a/webconsole/pom.xml b/webconsole/pom.xml
index e44f6aafb4..0762efa603 100644
--- a/webconsole/pom.xml
+++ b/webconsole/pom.xml
@@ -118,10 +118,10 @@
                         </Export-Package>
                         <!-- Import-Package header is also used for the all 
bundle -->
                         <Import-Package>
-                            javax.servlet.*;version="[3,5)",
+                            jakarta.servlet.*;version="[5,7)",
                             !javax.portlet,
-                            !jakarta.servlet,
-                            !jakarta.servlet.http,
+                            !javax.servlet,
+                            !javax.servlet.http,
                             !org.apache.felix.http.javaxwrappers,
                             !org.apache.felix.http.jakartawrappers,
                             !org.apache.felix.bundlerepository,
@@ -146,8 +146,8 @@
                             org.osgi.service.permissionadmin;version="[1.2,2)",
                             org.osgi.service.prefs;version="[1.1,2)",
                             org.osgi.service.wireadmin;version="[1.0,2)",
-                            jakarta.servlet;version="[5,7)",
-                            jakarta.servlet.http;version="[5,7)",
+                            javax.servlet;version="[3.1,5)",
+                            javax.servlet.http;version="[3.1,5)",
                             
org.apache.felix.http.javaxwrappers;version="[1,2)",
                             
org.apache.felix.http.jakartawrappers;version="[1,2)"
                         </DynamicImport-Package>
@@ -368,7 +368,7 @@
         <dependency>
             <groupId>jakarta.servlet</groupId>
             <artifactId>jakarta.servlet-api</artifactId>
-           <version>${servlet.api}</version>
+            <version>${servlet.api}</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java 
b/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java
index 2f6c2c6552..7b95414f16 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
 import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -38,6 +39,7 @@ import org.apache.commons.fileupload.FileUploadException;
 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import org.apache.commons.fileupload.servlet.ServletRequestContext;
+import org.apache.felix.webconsole.internal.Util;
 
 
 /**
@@ -325,19 +327,13 @@ public final class WebConsoleUtil
      * @param value the value to decode
      * @return the decoded string
      */
-    public static String urlDecode( final String value )
-    {
+    public static String urlDecode( final String value ) {
         // shortcut for empty or missing values
-        if ( value == null || value.length() == 0 )
-        {
+        if ( value == null || value.length() == 0 ) {
             return value;
         }
 
-        try {
-            return URLDecoder.decode( value, "UTF-8" );
-        } catch (UnsupportedEncodingException e) {
-            return URLDecoder.decode( value );
-        }
+        return URLDecoder.decode(value, StandardCharsets.UTF_8);
     }
 
     /**
@@ -351,35 +347,6 @@ public final class WebConsoleUtil
      * @return the string representation of the value
      */
     public static final String toString(Object value) {
-        if (value == null) {
-            return "n/a";
-        } else if (value.getClass().isArray()) {
-            final StringBuilder sb = new StringBuilder();
-            int len = Array.getLength(value);
-            sb.append('[');
-
-            for(int i = 0; i < len; ++i) {
-                final Object element = Array.get(value, i);
-                if (element instanceof Byte) {
-                    sb.append("0x");
-                    final String x = 
Integer.toHexString(((Byte)element).intValue() & 255);
-                    if (1 == x.length()) {
-                        sb.append('0');
-                    }
-
-                    sb.append(x);
-                } else {
-                    sb.append(toString(element));
-                }
-
-                if (i < len - 1) {
-                    sb.append(", ");
-                }
-            }
-
-            return sb.append(']').toString();
-        } else {
-            return value.toString();
-        }
+        return Util.toString(value);
      }
 }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java
index bc7bb81ba1..7ed3ea62ef 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java
@@ -18,8 +18,11 @@ package org.apache.felix.webconsole.internal;
 
 
 import java.io.IOException;
+import java.lang.reflect.Array;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.Locale;
 
 import org.osgi.framework.Bundle;
@@ -260,4 +263,74 @@ public class Util {
         }
         response.sendRedirect(redirectUrl);
     }
+
+    @SuppressWarnings("rawtypes")
+    public static String[] toStringArray( final Object value ) {
+        if ( value instanceof String ) {
+            return new String[] { ( String ) value };
+        } else if ( value != null ) {
+            final Collection col;
+            if ( value.getClass().isArray() ) {
+                col = Arrays.asList( ( Object[] ) value );
+            } else if ( value instanceof Collection ) {
+                col = ( Collection ) value;
+            } else {
+                col = null;
+            }
+
+            if ( col != null && !col.isEmpty() ) {
+                final String[] entries = new String[col.size()];
+                int i = 0;
+                for (final Iterator cli = col.iterator(); cli.hasNext(); i++ ) 
 {
+                    entries[i] = String.valueOf( cli.next() );
+                }
+                return entries;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * This method will stringify a Java object. It is mostly used to print 
the values
+     * of unknown properties. This method will correctly handle if the passed 
object
+     * is array and will property display it.
+     *
+     * If the value is byte[] the elements are shown as Hex
+     *
+     * @param value the value to convert
+     * @return the string representation of the value
+     */
+    public static final String toString(Object value) {
+        if (value == null) {
+            return "n/a";
+        } else if (value.getClass().isArray()) {
+            final StringBuilder sb = new StringBuilder();
+            int len = Array.getLength(value);
+            sb.append('[');
+
+            for(int i = 0; i < len; ++i) {
+                final Object element = Array.get(value, i);
+                if (element instanceof Byte) {
+                    sb.append("0x");
+                    final String x = 
Integer.toHexString(((Byte)element).intValue() & 255);
+                    if (1 == x.length()) {
+                        sb.append('0');
+                    }
+
+                    sb.append(x);
+                } else {
+                    sb.append(toString(element));
+                }
+
+                if (i < len - 1) {
+                    sb.append(", ");
+                }
+            }
+
+            return sb.append(']').toString();
+        } else {
+            return value.toString();
+        }
+     }
 }
\ No newline at end of file
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
index 2d52a5d57c..38506ab3fc 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java
@@ -361,9 +361,6 @@ public class ConfigManager extends 
AbstractOsgiManagerPlugin {
         super.doGet( request, response );
     }
 
-    /**
-     * @see 
org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)
-     */
     @Override
     public void renderContent( HttpServletRequest request, HttpServletResponse 
response ) throws IOException
     {
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
index f330899e4a..a684347baf 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
@@ -507,9 +507,6 @@ public class BundlesServlet extends 
AbstractOsgiManagerPlugin implements Invento
         buf.append(msg);
     }
 
-    /**
-     * @see 
org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)
-     */
     @Override
     public void renderContent( HttpServletRequest request, HttpServletResponse 
response ) throws IOException
     {
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
index 6abd54efd5..48c00e41fd 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java
@@ -451,10 +451,7 @@ public class ServicesServlet extends 
AbstractOsgiManagerPlugin
         super.doGet( request, response );
     }
 
-
-    /**
-     * @see 
org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)
-     */
+    @Override
     public void renderContent( HttpServletRequest request, HttpServletResponse 
response ) throws IOException {
         // get request info from request attribute
         final RequestInfo reqInfo = getRequestInfo( request );
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java
index 76f2b1dd2b..1be153c17c 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java
@@ -73,8 +73,6 @@ public class FilteringResponseWrapper extends 
HttpServletResponseWrapper {
      * is being generated a filtering writer is returned which translates
      * strings enclosed in <code>${}</code> according to the resource bundle
      * configured for this response.
-     *
-     * @see javax.servlet.ServletResponseWrapper#getWriter()
      */
     public PrintWriter getWriter() throws IOException {
         if ( writer == null ) {
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java
index 1a6798be31..e8850ab089 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java
@@ -20,25 +20,22 @@ package org.apache.felix.webconsole.internal.legacy;
 
 
 import java.io.Closeable;
-import java.net.URL;
 import java.util.Dictionary;
 import java.util.Hashtable;
 
 import javax.servlet.Servlet;
 
+import org.apache.felix.http.jakartawrappers.ServletWrapper;
 import org.apache.felix.http.javaxwrappers.HttpServletRequestWrapper;
 import org.apache.felix.http.javaxwrappers.HttpServletResponseWrapper;
-import org.apache.felix.http.javaxwrappers.ServletWrapper;
-import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
 import org.apache.felix.webconsole.WebConsoleSecurityProvider;
 import org.apache.felix.webconsole.WebConsoleSecurityProvider2;
 import org.apache.felix.webconsole.WebConsoleSecurityProvider3;
 import org.apache.felix.webconsole.internal.Util;
 import 
org.apache.felix.webconsole.internal.servlet.BasicWebConsoleSecurityProvider;
-import org.apache.felix.webconsole.internal.servlet.OsgiManager;
 import org.apache.felix.webconsole.internal.servlet.Plugin;
 import org.apache.felix.webconsole.internal.servlet.PluginHolder;
-import org.apache.felix.webconsole.servlet.AbstractServlet;
 import org.apache.felix.webconsole.servlet.ServletConstants;
 import org.apache.felix.webconsole.spi.SecurityProvider;
 import org.osgi.framework.BundleContext;
@@ -47,16 +44,17 @@ import org.osgi.framework.Filter;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.log.LogService;
 import org.osgi.util.tracker.ServiceTracker;
 import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 
+@SuppressWarnings("deprecation")
+public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomizer<Servlet, LegacyServicesTracker.LegacyServletPlugin> {
 
-public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomizer<Servlet, LegacyServicesTracker.JakartaServletPlugin> {
-
-    private final ServiceTracker<Servlet, JakartaServletPlugin> servletTracker;
+    private final ServiceTracker<Servlet, LegacyServletPlugin> servletTracker;
 
     private final PluginHolder pluginHolder;
 
@@ -76,7 +74,7 @@ public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomize
         }
         this.servletTracker = new ServiceTracker<>(context, filter, this);
         servletTracker.open();
-        this.securityProviderTracker = new 
LegacySecurityProviderTracker(context);
+        this.securityProviderTracker = new 
LegacySecurityProviderTracker(pluginHolder, context);
     }
 
     @Override
@@ -86,10 +84,13 @@ public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomize
     }
 
     @Override
-    public JakartaServletPlugin addingService( final ServiceReference<Servlet> 
reference ) {
-        final String label = Util.getStringProperty( reference, 
WebConsoleConstants.PLUGIN_LABEL );
+    public LegacyServletPlugin addingService( final ServiceReference<Servlet> 
reference ) {
+        final String label = Util.getStringProperty( reference, 
ServletConstants.PLUGIN_LABEL );
         if ( label != null ) {
-            final JakartaServletPlugin plugin = new 
JakartaServletPlugin(this.pluginHolder, reference, label);
+            this.pluginHolder.getOsgiManager().log(LogService.LOG_WARNING,
+                "Legacy webconsole plugin found. Update this to the Jakarta 
Servlet API: "  + reference);
+
+            final LegacyServletPlugin plugin = new 
LegacyServletPlugin(this.pluginHolder, reference, label);
             pluginHolder.addPlugin(plugin);
             return plugin;
         }
@@ -97,56 +98,55 @@ public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomize
     }
 
     @Override
-    public void modifiedService( final ServiceReference<Servlet> reference, 
final JakartaServletPlugin service ) {
+    public void modifiedService( final ServiceReference<Servlet> reference, 
final LegacyServletPlugin service ) {
         this.removedService(reference, service);
         this.addingService(reference);
     }
 
     @Override
-    public void removedService( final ServiceReference<Servlet> reference, 
final JakartaServletPlugin service ) {
+    public void removedService( final ServiceReference<Servlet> reference, 
final LegacyServletPlugin service ) {
         this.pluginHolder.removePlugin(service);
     }
 
-    public static class JakartaServletPlugin extends Plugin {
+    public static class LegacyServletPlugin extends Plugin {
             
-        @SuppressWarnings({"unchecked", "rawtypes"})
-        public JakartaServletPlugin(PluginHolder holder, 
ServiceReference<jakarta.servlet.Servlet> serviceReference,
-                String label) {
+        @SuppressWarnings({ "rawtypes", "unchecked"})
+        public LegacyServletPlugin(final PluginHolder holder, final 
ServiceReference<Servlet> serviceReference, final String label) {
             super(holder, (ServiceReference)serviceReference, label);
         }
 
-        protected jakarta.servlet.Servlet getService() {
-            final Servlet servlet = (Servlet) 
getHolder().getBundleContext().getService( 
(ServiceReference)this.getServiceReference() );
+        @SuppressWarnings({ "rawtypes", "unchecked"})
+        @Override
+        protected jakarta.servlet.Servlet doGetConsolePlugin() {
+            Servlet servlet = (Servlet) 
getHolder().getBundleContext().getService( 
(ServiceReference)this.getServiceReference() );
             if (servlet != null) {
-                if ( servlet instanceof AbstractServlet ) {
-                    return new JakartaServletAdapter((AbstractServlet)servlet, 
this.getServiceReference());
-                }
-                final String prefix = "/".concat(this.getLabel());
-                final String resStart = prefix.concat("/res/");
-                return new ServletWrapper(servlet) {
-
-                    @SuppressWarnings("unused")
-                    public URL getResource(String path) {
-                        if (path != null && path.startsWith(resStart)) {
-                            return 
servlet.getClass().getResource(path.substring(prefix.length()));
-                        }
-                        return null;
+                if ( servlet instanceof AbstractWebConsolePlugin ) {
+                    if (this.title == null) {
+                        this.title = 
((AbstractWebConsolePlugin)servlet).getTitle();
+                    }
+                    if (this.category == null) {
+                        this.category = 
((AbstractWebConsolePlugin)servlet).getCategory();
                     }
-                };
+                } else {
+                    servlet = new WebConsolePluginAdapter(getLabel(), servlet, 
(ServiceReference)this.getServiceReference());
+                }
+                return new ServletWrapper(servlet);
             }
             return null;
         }
     }
 
-    @SuppressWarnings("deprecation")
     public static class LegacySecurityProviderTracker implements 
ServiceTrackerCustomizer<WebConsoleSecurityProvider, 
ServiceRegistration<SecurityProvider>> {
 
         private final ServiceTracker<WebConsoleSecurityProvider, 
ServiceRegistration<SecurityProvider>> tracker;
 
         private final BundleContext bundleContext;
 
-        public LegacySecurityProviderTracker( final BundleContext context ) {
+        private final PluginHolder pluginHolder;
+
+        public LegacySecurityProviderTracker(final PluginHolder holder, final 
BundleContext context ) {
             this.bundleContext = context;
+            this.pluginHolder = holder;
             this.tracker = new ServiceTracker<>(context, 
WebConsoleSecurityProvider.class, this);
             tracker.open();
         }
@@ -157,6 +157,8 @@ public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomize
 
         @Override
         public ServiceRegistration<SecurityProvider> addingService( final 
ServiceReference<WebConsoleSecurityProvider> reference ) {
+            this.pluginHolder.getOsgiManager().log(LogService.LOG_WARNING,
+                "Legacy webconsole plugin found. Update this to the Jakarta 
Servlet API: "  + reference);
             final WebConsoleSecurityProvider provider = 
this.bundleContext.getService(reference);
             if ( provider != null ) {
                 final SecurityProvider wrapper = provider instanceof 
WebConsoleSecurityProvider2 
@@ -178,8 +180,8 @@ public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomize
                 if (reference.getProperty(Constants.SERVICE_RANKING) != null) {
                     props.put(Constants.SERVICE_RANKING, 
reference.getProperty(Constants.SERVICE_RANKING));
                 }
-                if 
(reference.getProperty(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME) != null) {
-                    props.put(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME, 
reference.getProperty(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME));
+                if (reference.getProperty(SecurityProvider.PROPERTY_ID) != 
null) {
+                    props.put(SecurityProvider.PROPERTY_ID, 
reference.getProperty(SecurityProvider.PROPERTY_ID));
                 }
                 return 
reference.getBundle().getBundleContext().registerService(SecurityProvider.class,
 wrapper, props);
             }
@@ -202,7 +204,6 @@ public class LegacyServicesTracker implements Closeable, 
ServiceTrackerCustomize
         }
     }
 
-    @SuppressWarnings("deprecation")
     public static class SecurityProviderWrapper implements SecurityProvider {
             
         private final WebConsoleSecurityProvider provider;
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/WebConsolePluginAdapter.java
similarity index 84%
rename from 
webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
rename to 
webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/WebConsolePluginAdapter.java
index 37aa34687d..6386ea176d 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/WebConsolePluginAdapter.java
@@ -16,18 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.felix.webconsole.internal;
+package org.apache.felix.webconsole.internal.legacy;
 
 
 import java.io.IOException;
-import java.util.*;
-
-import javax.servlet.*;
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.felix.webconsole.AbstractWebConsolePlugin;
 import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.felix.webconsole.internal.Util;
+import org.apache.felix.webconsole.servlet.ServletConstants;
 import org.osgi.framework.ServiceReference;
 
 
@@ -37,6 +41,7 @@ import org.osgi.framework.ServiceReference;
  * {@link org.apache.felix.webconsole.WebConsoleConstants#PLUGIN_TITLE}
  * service attribute.
  */
+@SuppressWarnings("deprecation")
 public class WebConsolePluginAdapter extends AbstractWebConsolePlugin
 {
 
@@ -63,7 +68,7 @@ public class WebConsolePluginAdapter extends 
AbstractWebConsolePlugin
     {
         this.label = label;
         this.plugin = plugin;
-        this.cssReferences = toStringArray( serviceReference.getProperty( 
WebConsoleConstants.PLUGIN_CSS_REFERENCES ) );
+        this.cssReferences = Util.toStringArray( serviceReference.getProperty( 
ServletConstants.PLUGIN_CSS_REFERENCES ) );
 
         // activate this abstract plugin (mainly to set the bundle context)
         activate( serviceReference.getBundle().getBundleContext() );
@@ -235,46 +240,4 @@ public class WebConsolePluginAdapter extends 
AbstractWebConsolePlugin
             deactivate();
         }
     }
-
-
-    //---------- internal
-
-    @SuppressWarnings("rawtypes")
-    private String[] toStringArray( final Object value )
-    {
-        if ( value instanceof String )
-        {
-            return new String[]
-                { ( String ) value };
-        }
-        else if ( value != null )
-        {
-            final Collection cssListColl;
-            if ( value.getClass().isArray() )
-            {
-                cssListColl = Arrays.asList( ( Object[] ) value );
-            }
-            else if ( value instanceof Collection )
-            {
-                cssListColl = ( Collection ) value;
-            }
-            else
-            {
-                cssListColl = null;
-            }
-
-            if ( cssListColl != null && !cssListColl.isEmpty() )
-            {
-                String[] entries = new String[cssListColl.size()];
-                int i = 0;
-                for ( Iterator cli = cssListColl.iterator(); cli.hasNext(); 
i++ )
-                {
-                    entries[i] = String.valueOf( cli.next() );
-                }
-                return entries;
-            }
-        }
-
-        return null;
-    }
 }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
index ac3aed49ed..1edd8f3c02 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java
@@ -19,11 +19,9 @@ package org.apache.felix.webconsole.internal.misc;
 
 import java.io.PrintWriter;
 
-import org.apache.felix.webconsole.WebConsoleUtil;
+import org.apache.felix.webconsole.internal.Util;
 
-
-public class ConfigurationRender
-{
+public class ConfigurationRender {
 
     /**
      * Renders an info line - element in the framework configuration. The info 
line will
@@ -41,8 +39,7 @@ public class ConfigurationRender
      */
     public static final void infoLine( PrintWriter pw, String indent, String 
label, Object value )
     {
-        if ( indent != null )
-        {
+        if ( indent != null ) {
             pw.print( indent );
         }
 
@@ -52,7 +49,7 @@ public class ConfigurationRender
             pw.print( " = " );
         }
 
-        pw.print( WebConsoleUtil.toString( value ) );
+        pw.print( Util.toString( value ) );
 
         pw.println();
     }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractInternalPlugin.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractInternalPlugin.java
deleted file mode 100644
index b2198e6084..0000000000
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractInternalPlugin.java
+++ /dev/null
@@ -1,984 +0,0 @@
-/*
- * 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.felix.webconsole.internal.servlet;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.net.URL;
-import java.net.URLConnection;
-import java.nio.charset.StandardCharsets;
-import java.security.AccessController;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.Dictionary;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Iterator;
-import java.util.Locale;
-import java.util.Map;
-import java.util.ResourceBundle;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import org.apache.felix.webconsole.DefaultBrandingPlugin;
-import org.apache.felix.webconsole.SimpleWebConsolePlugin;
-import org.apache.felix.webconsole.WebConsoleConstants;
-import org.apache.felix.webconsole.WebConsoleUtil;
-import org.apache.felix.webconsole.i18n.LocalizationHelper;
-import org.apache.felix.webconsole.internal.servlet.OsgiManager;
-import org.apache.felix.webconsole.servlet.RequestVariableResolver;
-import org.apache.felix.webconsole.servlet.ServletConstants;
-import org.apache.felix.webconsole.spi.BrandingPlugin;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceReference;
-import org.osgi.framework.ServiceRegistration;
-import org.osgi.service.log.LogService;
-import org.osgi.util.tracker.ServiceTracker;
-import org.osgi.util.tracker.ServiceTrackerCustomizer;
-
-import jakarta.servlet.Servlet;
-import jakarta.servlet.ServletConfig;
-import jakarta.servlet.ServletContext;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-/**
- * This is an internal base class for web console plugins
- */
-public abstract class AbstractInternalPlugin extends HttpServlet {
-
-    /**
-     * The name of the request attribute providing a mapping of labels to page
-     * titles of registered console plugins (value is 
"felix.webconsole.labelMap").
-     * This map may be used to render a navigation of the console plugins as 
the
-     * {@link 
AbstractWebConsolePlugin#renderTopNavigation(javax.servlet.http.HttpServletRequest,
 java.io.PrintWriter)}
-     * method does.
-     * <p>
-     * The type of this request attribute is <code>Map&lt;String, 
String&gt;</code>.
-     */
-    public static final String ATTR_LABEL_MAP = "felix.webconsole.labelMap";
-
-    /**
-     * The header fragment read from the templates/main_header.html file
-     */
-    private static String HEADER;
-
-    /**
-     * The footer fragment read from the templates/main_footer.html file
-     */
-    private static String FOOTER;
-
-    private static volatile BrandingPlugin BRANDING_PLUGIN = 
DefaultBrandingPlugin.getInstance();
-
-    private static volatile int LOGLEVEL;
-
-    private final String css[];
-
-    private final String labelRes;
-    private final int labelResLen;
-
-    // localized title as servlet name
-    private volatile String servletName;
-
-    private volatile BundleContext bundleContext;
-
-    // used for service registration
-    private final Object regLock = new Object();
-    private volatile ServiceRegistration<Servlet> reg;
-
-    // used to obtain services. Structure is: service name -> ServiceTracker
-    private final Map<String, ServiceTracker<?, ?>> services = new HashMap<>();
-
-    /**
-     * Creates new plugin
-     *
-     * @param label the front label
-     * @param title the plugin title
-     * @param category the plugin's navigation category (optional)
-     * @param css the additional plugin CSS (optional)
-     * @throws NullPointerException if either <code>label</code> or 
<code>title</code> is <code>null</code>
-     */
-    public AbstractInternalPlugin( final String label, final String css[] ) {
-        if ( label == null ) {
-            throw new NullPointerException( "Null label" );
-        }
-        this.css = css;
-        this.labelRes = '/' + label + '/';
-        this.labelResLen = labelRes.length() - 1;
-    }
-
-    public void activate(final BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
-        if (this.title.startsWith( "%" ) ) {
-            // FELIX-6341 - dig out the localized title for use in log messages
-            final Bundle bundle = bundleContext.getBundle();
-            if (bundle != null) {
-                final LocalizationHelper localization = new 
LocalizationHelper( bundle );
-                final ResourceBundle rb = 
localization.getResourceBundle(Locale.getDefault());
-                if (rb != null) {
-                    final String key = this.title.substring(1);
-                    if (rb.containsKey(key)) {
-                        this.servletName = rb.getString(key);
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    public String getServletName() {
-        // use the localized title if we have one
-        if (this.servletName != null) {
-            return this.servletName;
-        }
-        return this.getTitle();
-    }
-
-    public final String getLabel() {
-        return label;
-    }
-
-    public final String getTitle() {
-        return title;
-    }
-
-    public String getCategory() {
-        return category;
-    }
-
-    /**
-     * Called internally to load resources.
-     *
-     * This particular implementation depends on the label. As example, if the
-     * plugin is accessed as <code>/system/console/abc</code>, and the plugin
-     * resources are accessed like 
<code>/system/console/abc/res/logo.gif</code>,
-     * the code here will try load resource <code>/res/logo.gif</code> from the
-     * bundle, providing the plugin.
-     *
-     *
-     * @param path the path to read.
-     * @return the URL of the resource or <code>null</code> if not found.
-     */
-    protected URL getResource( String path ) {
-        return ( path != null && path.startsWith( labelRes ) ) ? //
-            getClass().getResource( path.substring( labelResLen ) )
-            : null;
-    }
-
-    /**
-     * This is an utility method. It is used to register the plugin service. 
Don't
-     * forget to call the {@link #unregister()} when the plugin is no longer
-     * needed.
-     *
-     * @param bc the bundle context used for service registration.
-     * @return self
-     */
-    public final AbstractInternalPlugin register() {
-        synchronized ( regLock ) {
-            final Dictionary<String, Object> props = new Hashtable<>();
-            props.put( ServletConstants.PLUGIN_LABEL, getLabel() );
-            props.put( ServletConstants.PLUGIN_TITLE, getTitle() );
-            if ( getCategory() != null ) {
-                props.put( ServletConstants.PLUGIN_CATEGORY, getCategory() );
-            }
-            if ( this.css != null && this.css.length > 0 ) {
-                props.put( ServletConstants.PLUGIN_CSS_REFERENCES, this.css );
-            }
-            reg = this.bundleContext.registerService( Servlet.class, this, 
props );
-        }
-        return this;
-    }
-
-    /**
-     * An utility method that removes the service, registered by the
-     * {@link #register()} method.
-     */
-    public final void unregister() {
-        synchronized ( regLock ) {
-            if ( reg != null ) {
-                try {
-                    reg.unregister();
-                } catch ( final IllegalStateException ise ) {
-                    // ignore, bundle context already invalid
-                }
-            }
-            reg = null;
-        }
-    }
-
-    // -- begin methods for obtaining services
-
-    /**
-     * Gets the service with the specified class name. Will create a new
-     * {@link ServiceTracker} if the service is not already got.
-     *
-     * @param serviceName the service name to obtain
-     * @return the service or <code>null</code> if missing.
-     */
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    public final Object getService( String serviceName ) {
-        ServiceTracker<?,?> serviceTracker = services.get( serviceName );
-        if ( serviceTracker == null ) {
-            serviceTracker = new ServiceTracker( getBundleContext(), 
serviceName, new ServiceTrackerCustomizer() {
-                    public Object addingService( ServiceReference reference ) {
-                        return getBundleContext().getService( reference );
-                    }
-
-                    public void removedService( ServiceReference reference, 
Object service ) {
-                        try {
-                            getBundleContext().ungetService( reference );
-                        } catch ( IllegalStateException ise ) {
-                            // ignore, bundle context was shut down
-                        }
-                    }
-
-                    public void modifiedService( ServiceReference reference, 
Object service ) {
-                        // nothing to do
-                    }
-            } );
-            serviceTracker.open();
-
-            services.put( serviceName, serviceTracker );
-        }
-
-        return serviceTracker.getService();
-    }
-
-
-    /**
-     * This method will close all service trackers, created by
-     * {@link #getService(String)} method. If you override this method, don't
-     * forget to call the super.
-     */
-    public void deactivate() {
-        for ( Iterator<ServiceTracker<?, ?>> ti = 
services.values().iterator(); ti.hasNext(); ) {
-            ServiceTracker<?, ?> tracker = ti.next();
-            tracker.close();
-            ti.remove();
-        }
-        this.bundleContext = null;
-    }
-
-    //---------- HttpServlet Overwrites 
----------------------------------------
-
-    /**
-     * Renders the web console page for the request. This consist of the
-     * following five parts called in order:
-     * <ol>
-     * <li>Send back a requested resource
-     * <li>{@link #startResponse(HttpServletRequest, HttpServletResponse)}</li>
-     * <li>{@link #renderTopNavigation(HttpServletRequest, PrintWriter)}</li>
-     * <li>{@link #renderContent(HttpServletRequest, HttpServletResponse)}</li>
-     * <li>{@link #endResponse(PrintWriter)}</li>
-     * </ol>
-     * <p>
-     * <b>Note</b>: If a resource is sent back for the request only the first
-     * step is executed. Otherwise the first step is a null-operation actually
-     * and the latter four steps are executed in order.
-     * <p>
-     * If the {@link #isHtmlRequest(HttpServletRequest)} method returns
-     * <code>false</code> only the
-     * {@link #renderContent(HttpServletRequest, HttpServletResponse)} method 
is
-     * called.
-     *
-     * @see 
javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
-     *      javax.servlet.http.HttpServletResponse)
-     */
-    protected void doGet( HttpServletRequest request, HttpServletResponse 
response )
-    throws ServletException, IOException {
-        if ( !spoolResource( request, response ) ) {
-            // detect if this is an html request
-            if ( isHtmlRequest(request) ) {
-                // start the html response, write the header, open body and 
main div
-                PrintWriter pw = startResponse( request, response );
-
-                // render top navigation
-                renderTopNavigation( request, pw );
-
-                // wrap content in a separate div
-                pw.println( "<div id='content'>" );
-                renderContent( request, response );
-                pw.println( "</div>" );
-
-                // close the main div, body, and html
-                endResponse( pw );
-            } else {
-                renderContent( request, response );
-            }
-        }
-    }
-
-    /**
-     * Detects whether this request is intended to have the headers and
-     * footers of this plugin be rendered or not. This method always returns
-     * <code>true</code> and may be overwritten by plugins to detect
-     * from the actual request, whether or not to render the header and
-     * footer.
-     *
-     * @param request the original request passed from the HTTP server
-     * @return <code>true</code> if the page should have headers and footers 
rendered
-     */
-    protected boolean isHtmlRequest( final HttpServletRequest request ) {
-        return true;
-    }
-
-    /**
-     * This method is used to render the content of the plug-in. It is called 
internally
-     * from the Web Console.
-     *
-     * @param req the HTTP request send from the user
-     * @param res the HTTP response object, where to render the plugin data.
-     * @throws IOException if an input or output error is
-     *  detected when the servlet handles the request
-     * @throws ServletException  if the request for the GET
-     *  could not be handled
-     */
-    protected abstract void renderContent( HttpServletRequest req, 
HttpServletResponse res )
-    throws ServletException, IOException;
-
-    /**
-     * Returns the <code>BundleContext</code> with which this plugin has been
-     * activated. If the plugin has not be activated by calling the
-     * {@link #activate(BundleContext)} method, this method returns
-     * <code>null</code>.
-     *
-     * @return the bundle context or <code>null</code> if the bundle is not 
activated.
-     */
-    protected BundleContext getBundleContext()  {
-        return bundleContext;
-    }
-
-    /**
-     * Returns the <code>Bundle</code> pertaining to the
-     * {@link #getBundleContext() bundle context} with which this plugin has
-     * been activated. If the plugin has not be activated by calling the
-     * {@link #activate(BundleContext)} method, this method returns
-     * <code>null</code>.
-     *
-     * @return the bundle or <code>null</code> if the plugin is not activated.
-     */
-    protected final Bundle getBundle() {
-        final BundleContext bundleContext = getBundleContext();
-        return ( bundleContext != null ) ? bundleContext.getBundle() : null;
-    }
-
-    /**
-     * Calls the <code>ServletContext.log(String)</code> method if the
-     * configured log level is less than or equal to the given 
<code>level</code>.
-     * <p>
-     * Note, that the <code>level</code> paramter is only used to decide 
whether
-     * the <code>GenericServlet.log(String)</code> method is called or not. The
-     * actual implementation of the <code>GenericServlet.log</code> method is
-     * outside of the control of this method.
-     * <p>
-     * If the servlet has not been initialized yet or has already been 
destroyed
-     * the message is printed to stderr.
-     *
-     * @param level The log level at which to log the message
-     * @param message The message to log
-     */
-    public void log( int level, String message ) {
-        if ( LOGLEVEL >= level ) {
-            ServletConfig config = getServletConfig();
-            if ( config != null ) {
-                ServletContext context = config.getServletContext();
-                if ( context != null ) {
-                    context.log( message );
-                    return;
-                }
-            }
-
-            System.err.println( message );
-        }
-    }
-
-
-    /**
-     * Calls the <code>ServletContext.log(String, Throwable)</code> method if
-     * the configured log level is less than or equal to the given
-     * <code>level</code>.
-     * <p>
-     * Note, that the <code>level</code> paramter is only used to decide 
whether
-     * the <code>GenericServlet.log(String, Throwable)</code> method is called
-     * or not. The actual implementation of the <code>GenericServlet.log</code>
-     * method is outside of the control of this method.
-     *
-     * @param level The log level at which to log the message
-     * @param message The message to log
-     * @param t The <code>Throwable</code> to log with the message
-     */
-    public void log( int level, String message, Throwable t ) {
-        if ( LOGLEVEL >= level ) {
-            ServletConfig config = getServletConfig();
-            if ( config != null ) {
-                ServletContext context = config.getServletContext();
-                if ( context != null ) {
-                    context.log( message, t );
-                    return;
-                }
-            }
-
-            System.err.println( message );
-            if ( t != null ) {
-                t.printStackTrace( System.err );
-            }
-        }
-    }
-    
-    /**
-     * If the request addresses a resource which may be served by the
-     * <code>getResource</code> method of the
-     * {@link #getResourceProvider() resource provider}, this method serves it
-     * and returns <code>true</code>. Otherwise <code>false</code> is returned.
-     * <code>false</code> is also returned if the resource provider has no
-     * <code>getResource</code> method.
-     * <p>
-     * If <code>true</code> is returned, the request is considered complete and
-     * request processing terminates. Otherwise request processing continues
-     * with normal plugin rendering.
-     *
-     * @param request The request object
-     * @param response The response object
-     * @return <code>true</code> if the request causes a resource to be sent 
back.
-     *
-     * @throws IOException If an error occurs accessing or spooling the 
resource.
-     */
-    private boolean spoolResource(final HttpServletRequest request, 
-        final HttpServletResponse response) throws IOException {
-        try {
-            // We need to call spoolResource0 in privileged block because it 
uses reflection, which
-            // requires the following set of permissions:
-            // (java.lang.RuntimePermission "getClassLoader")
-            // (java.lang.RuntimePermission "accessDeclaredMembers")
-            // (java.lang.reflect.ReflectPermission "suppressAccessChecks")
-            // See also https://issues.apache.org/jira/browse/FELIX-4652
-            final Boolean ret = AccessController.doPrivileged(new 
PrivilegedExceptionAction<Boolean>() {
-
-                public Boolean run() throws Exception {
-                    return spoolResource0(request, response) ? Boolean.TRUE : 
Boolean.FALSE;
-                }
-            });
-            return ret.booleanValue();
-        } catch (final PrivilegedActionException e) {
-            final Exception x = e.getException();
-            throw x instanceof IOException ? (IOException) x : new 
IOException(x.toString());
-        }
-    }
-
-    private boolean spoolResource0(final HttpServletRequest request, final 
HttpServletResponse response ) throws IOException {
-        final URL url = getResource(request.getPathInfo());
-        if ( url == null ) {
-            return false;
-        }
-        // open the connection and the stream (we use the stream to be able
-        // to at least hint to close the connection because there is no
-        // method to explicitly close the conneciton, unfortunately)
-        final URLConnection connection = url.openConnection();
-        try ( InputStream ins = connection.getInputStream()) {
-            // FELIX-2017 Equinox may return an URL for a non-existing
-            // resource but then (instead of throwing) return null on
-            // getInputStream. We should account for this situation and
-            // just assume a non-existing resource in this case.
-            if (ins == null) {
-                return false;
-            }
-
-            // check whether we may return 304/UNMODIFIED
-            long lastModified = connection.getLastModified();
-            if ( lastModified > 0 ) {
-                long ifModifiedSince = request.getDateHeader( 
"If-Modified-Since" );
-                if ( ifModifiedSince >= ( lastModified / 1000 * 1000 ) ) {
-                    // Round down to the nearest second for a proper compare
-                    // A ifModifiedSince of -1 will always be less
-                    response.setStatus( HttpServletResponse.SC_NOT_MODIFIED );
-
-                    return true;
-                }
-
-                // have to send, so set the last modified header now
-                response.setDateHeader( "Last-Modified", lastModified );
-            }
-
-            // describe the contents
-            response.setContentType( getServletContext().getMimeType( 
request.getPathInfo() ) );
-            response.setIntHeader( "Content-Length", 
connection.getContentLength() );
-
-            // spool the actual contents
-            OutputStream out = response.getOutputStream();
-            byte[] buf = new byte[2048];
-            int rd;
-            while ( ( rd = ins.read( buf ) ) >= 0 ) {
-                out.write( buf, 0, rd );
-            }
-
-            return true;
-        }
-    }
-
-    /**
-     * This method is responsible for generating the top heading of the page.
-     *
-     * @param request the HTTP request coming from the user
-     * @param response the HTTP response, where data is rendered
-     * @return the writer that was used for generating the response.
-     * @throws IOException on I/O error
-     * @see #endResponse(PrintWriter)
-     */
-    private PrintWriter startResponse( HttpServletRequest request, 
HttpServletResponse response ) throws IOException {
-        response.setCharacterEncoding( "utf-8" );
-        response.setContentType( "text/html" );
-
-        final PrintWriter pw = response.getWriter();
-
-        final String appRoot = ( String ) request.getAttribute( 
ServletConstants.ATTR_APP_ROOT );
-
-        // support localization of the plugin title
-        String title = getTitle();
-        if ( title.startsWith( "%" ) ) {
-            title = "${" + title.substring( 1 ) + "}";
-        }
-
-        final RequestVariableResolver r = this.getVariableResolver(request);
-        r.put("head.title", title);
-        r.put("head.label", getLabel());
-        r.put("head.cssLinks", getCssLinks(appRoot));
-        r.put("brand.name", BRANDING_PLUGIN.getBrandName());
-        r.put("brand.product.url", BRANDING_PLUGIN.getProductURL());
-        r.put("brand.product.name", BRANDING_PLUGIN.getProductName());
-        r.put("brand.product.img", toUrl( BRANDING_PLUGIN.getProductImage(), 
appRoot ));
-        r.put("brand.favicon", toUrl( BRANDING_PLUGIN.getFavIcon(), appRoot ));
-        r.put("brand.css", toUrl( BRANDING_PLUGIN.getMainStyleSheet(), appRoot 
));
-        pw.println( getHeader() );
-
-        return pw;
-    }
-
-
-    /**
-     * This method is called to generate the top level links with the 
available plug-ins.
-     *
-     * @param request the HTTP request coming from the user
-     * @param pw the writer, where the HTML data is rendered
-     */
-    @SuppressWarnings({ "rawtypes" })
-    private void renderTopNavigation( HttpServletRequest request, PrintWriter 
pw ) {
-        // assume pathInfo to not be null, else this would not be called
-        String current = request.getPathInfo();
-        int slash = current.indexOf( "/", 1 );
-        if ( slash < 0 ) {
-            slash = current.length();
-        }
-        current = current.substring( 1, slash );
-
-        String appRoot = ( String ) request.getAttribute( 
ServletConstants.ATTR_APP_ROOT );
-
-        Map menuMap = ( Map ) request.getAttribute( 
OsgiManager.ATTR_LABEL_MAP_CATEGORIZED );
-        this.renderMenu( menuMap, appRoot, pw );
-
-        // render lang-box
-        Map langMap = (Map) 
request.getAttribute(WebConsoleConstants.ATTR_LANG_MAP);
-        if (null != langMap && !langMap.isEmpty())
-        {
-            // determine the currently selected locale from the request and 
fail-back
-            // to the default locale if not set
-            // if locale is missing in locale map, the default 'en' locale is 
used
-            Locale reqLocale = request.getLocale();
-            String locale = null != reqLocale ? reqLocale.getLanguage()
-                : Locale.getDefault().getLanguage();
-            if (!langMap.containsKey(locale))
-            {
-                locale = Locale.getDefault().getLanguage();
-            }
-            if (!langMap.containsKey(locale))
-            {
-                locale = "en";
-            }
-
-            pw.println("<div id='langSelect'>");
-            pw.println(" <span>");
-            printLocaleElement(pw, appRoot, locale, langMap.get(locale));
-            pw.println(" </span>");
-            pw.println(" <span class='flags ui-helper-hidden'>");
-            for (Iterator li = langMap.keySet().iterator(); li.hasNext();)
-            {
-                // <img src="us.gif" alt="en" title="English"/>
-                final Object l = li.next();
-                if (!l.equals(locale))
-                {
-                    printLocaleElement(pw, appRoot, l, langMap.get(l));
-                }
-            }
-
-            pw.println(" </span>");
-            pw.println("</div>");
-        }
-    }
-
-
-    @SuppressWarnings({ "rawtypes" })
-    protected void renderMenu( Map menuMap, String appRoot, PrintWriter pw )
-    {
-        if ( menuMap != null )
-        {
-            SortedMap categoryMap = sortMenuCategoryMap( menuMap, appRoot );
-            pw.println( "<ul id=\"navmenu\">" );
-            renderSubmenu( categoryMap, appRoot, pw, 0 );
-            pw.println("<li class=\"logoutButton navMenuItem-0\">");
-            pw.println("<a href=\"" + appRoot + "/logout\">${logout}</a>");
-            pw.println("</li>");
-            pw.println( "</ul>" );
-        }
-    }
-
-
-    @SuppressWarnings({ "rawtypes" })
-    private void renderMenu( Map menuMap, String appRoot, PrintWriter pw, int 
level )
-    {
-        pw.println( "<ul class=\"navMenuLevel-" + level + "\">" );
-        renderSubmenu( menuMap, appRoot, pw, level );
-        pw.println( "</ul>" );
-    }
-
-
-    @SuppressWarnings({ "rawtypes" })
-    private void renderSubmenu( Map menuMap, String appRoot, PrintWriter pw, 
int level )
-    {
-        String liStyleClass = " class=\"navMenuItem-" + level + "\"";
-        Iterator itr = menuMap.keySet().iterator();
-        while ( itr.hasNext() )
-        {
-            String key = ( String ) itr.next();
-            MenuItem menuItem = ( MenuItem ) menuMap.get( key );
-            pw.println( "<li" + liStyleClass + ">" + menuItem.getLink() );
-            Map subMenu = menuItem.getSubMenu();
-            if ( subMenu != null )
-            {
-                renderMenu( subMenu, appRoot, pw, level + 1 );
-            }
-            pw.println( "</li>" );
-        }
-    }
-
-
-    private static final void printLocaleElement( PrintWriter pw, String 
appRoot, Object langCode, Object langName )
-    {
-        pw.print("  <img src='");
-        pw.print(appRoot);
-        pw.print("/res/flags/");
-        pw.print(langCode);
-        pw.print(".gif' alt='");
-        pw.print(langCode);
-        pw.print("' title='");
-        pw.print(langName);
-        pw.println("'/>");
-    }
-
-    /**
-     * This method is responsible for generating the footer of the page.
-     *
-     * @param pw the writer, where the HTML data is rendered
-     * @see #startResponse(HttpServletRequest, HttpServletResponse)
-     */
-    protected void endResponse( PrintWriter pw )
-    {
-        pw.println(getFooter());
-    }
-
-    /**
-     * Sets the {@link BrandingPlugin} to use globally by all extensions of
-     * this class for branding.
-     * <p>
-     * Note: This method is intended to be used internally by the Web Console
-     * to update the branding plugin to use.
-     *
-     * @param brandingPlugin the brandingPlugin to set
-     * @deprecated
-     */
-    @Deprecated
-    public static final void setBrandingPlugin(final BrandingPlugin 
brandingPlugin) {
-        if (brandingPlugin == null){
-            AbstractInternalPlugin.BRANDING_PLUGIN = 
DefaultBrandingPlugin.getInstance();
-        } else {
-            AbstractInternalPlugin.BRANDING_PLUGIN = brandingPlugin;
-        }
-    }
-
-    /**
-     * Sets the log level to be applied for calls to the {@link #log(int, 
String)}
-     * and {@link #log(int, String, Throwable)} methods.
-     * <p>
-     * Note: This method is intended to be used internally by the Web Console
-     * to update the log level according to the Web Console configuration.
-     *
-     * @param logLevel the maximum allowed log level. If message is logged with
-     *        lower level it will not be forwarded to the logger.
-     */
-    public static final void setLogLevel( final int logLevel ) {
-        AbstractInternalPlugin.LOGLEVEL = logLevel;
-    }
-
-    private final String getHeader() {
-        // MessageFormat pattern place holder
-        //  0 main title (brand name)
-        //  1 console plugin title
-        //  2 application root path (ATTR_APP_ROOT)
-        //  3 console plugin label (from the URI)
-        //  4 branding favourite icon (BrandingPlugin.getFavIcon())
-        //  5 branding main style sheet (BrandingPlugin.getMainStyleSheet())
-        //  6 branding product URL (BrandingPlugin.getProductURL())
-        //  7 branding product name (BrandingPlugin.getProductName())
-        //  8 branding product image (BrandingPlugin.getProductImage())
-        //  9 additional HTML code to be inserted into the <head> section
-        //    (for example plugin provided CSS links)
-        if ( HEADER == null )
-        {
-            HEADER = readTemplateFile( AbstractInternalPlugin.class, 
"/templates/main_header.html" );
-        }
-        return HEADER;
-    }
-
-
-    private final String getFooter()
-    {
-        if ( FOOTER == null )
-        {
-            FOOTER = readTemplateFile( AbstractInternalPlugin.class, 
"/templates/main_footer.html" );
-        }
-        return FOOTER;
-    }
-
-    /**
-     * Reads the <code>templateFile</code> as a resource through the class
-     * loader of this class converting the binary data into a string using
-     * UTF-8 encoding.
-     * <p>
-     * If the template file cannot read into a string and an exception is
-     * caused, the exception is logged and an empty string returned.
-     *
-     * @param templateFile The absolute path to the template file to read.
-     * @return The contents of the template file as a string or and empty
-     *      string if the template file fails to be read.
-     *
-     * @throws NullPointerException if <code>templateFile</code> is
-     *      <code>null</code>
-     * @throws RuntimeException if an <code>IOException</code> is thrown 
reading
-     *      the template file into a string. The exception provides the
-     *      exception thrown as its cause.
-     */
-    protected final String readTemplateFile( final String templateFile ) {
-        return readTemplateFile( getClass(), templateFile );
-    }
-
-    /**
-     * Retrieves a request parameter and converts it to int.
-     *
-     * @param request the HTTP request
-     * @param name the name of the request parameter
-     * @param defaultValue the default value returned if the parameter is not 
set or is not a valid integer.
-     * @return the request parameter if set and is valid integer, or the 
default value
-     */
-    protected static final int getParameterInt(final HttpServletRequest 
request, final String name, final int defaultValue) {
-        int ret = defaultValue;
-        final String param = request.getParameter(name);
-        if (param != null) {
-            try {
-                ret = Integer.parseInt(param);
-            } catch (final NumberFormatException nfe) {
-            // don't care, will return default
-            }
-        }
-        return ret;
-    }
-
-    protected void sendJsonOk(final HttpServletResponse response) throws 
IOException {
-        response.setContentType( "application/json" );
-        response.setCharacterEncoding( "UTF-8" );
-        response.getWriter().print( "{ \"status\": true }" );
-    }
-
-    private final String readTemplateFile( final Class<?> clazz, final String 
templateFile) {
-        
-        try(InputStream templateStream = clazz.getResourceAsStream( 
templateFile )) {
-            if ( templateStream != null ) {
-                try ( final StringWriter w = new StringWriter()) {
-                    final byte[] buf = new byte[2048];
-                    int l;
-                    while ( ( l = templateStream.read(buf)) > 0 ) {
-                        w.write(new String(buf, 0, l, StandardCharsets.UTF_8));
-                    }
-                    String str = w.toString();
-                    switch ( str.charAt(0) )
-                    { // skip BOM
-                        case 0xFEFF: // UTF-16/UTF-32, big-endian
-                        case 0xFFFE: // UTF-16, little-endian
-                        case 0xEFBB: // UTF-8
-                            return str.substring(1);
-                    }
-                    return str;
-                    }
-            }
-        }
-        catch ( IOException e )
-        {
-            // don't use new Exception(message, cause) because cause is 1.4+
-            throw new RuntimeException( "readTemplateFile: Error loading " + 
templateFile + ": " + e );
-        }
-
-        // template file does not exist, return an empty string
-        log( LogService.LOG_ERROR, "readTemplateFile: File '" + templateFile + 
"' not found through class " + clazz );
-        return "";
-    }
-
-    private final String getCssLinks( final String appRoot ) {
-        if ( css == null || css.length == 0) {
-            return "";
-        }
-
-        // build the CSS links from the references
-        final StringBuilder buf = new StringBuilder();
-        for(final String ref : css) {
-            buf.append( "<link href='" );
-            buf.append( toUrl( ref, appRoot ) );
-            buf.append( "' rel='stylesheet' type='text/css' />" );
-        }
-
-        return buf.toString();
-    }
-
-    /**
-     * If the <code>url</code> starts with a slash, it is considered an 
absolute
-     * path (relative URL) which must be prefixed with the Web Console
-     * application root path. Otherwise the <code>url</code> is assumed to
-     * either be a relative path or an absolute URL, both must not be prefixed.
-     *
-     * @param url The url path to optionally prefix with the application root
-     *          path
-     * @param appRoot The application root path to optionally put in front of
-     *          the url.
-     * @throws NullPointerException if <code>url</code> is <code>null</code>.
-     */
-    private static final String toUrl( final String url, final String appRoot 
) {
-        if ( url.startsWith( "/" ) )
-        {
-            return appRoot.concat(url);
-        }
-        return url;
-    }
-
-
-    @SuppressWarnings({ "unchecked", "rawtypes" })
-    private SortedMap sortMenuCategoryMap( Map map, String appRoot )
-    {
-        SortedMap sortedMap = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
-        Iterator keys = map.keySet().iterator();
-        while ( keys.hasNext() )
-        {
-            String key = ( String ) keys.next();
-            if ( key.startsWith( "category." ) )
-            {
-                SortedMap categoryMap = sortMenuCategoryMap( ( Map ) map.get( 
key ), appRoot );
-                String title = key.substring( key.indexOf( '.' ) + 1 );
-                if ( sortedMap.containsKey( title ) )
-                {
-                    ( ( MenuItem ) sortedMap.get( title ) ).setSubMenu( 
categoryMap );
-                }
-                else
-                {
-                    String link = "<a href=\"#\">" + title + "</a>";
-                    MenuItem menuItem = new MenuItem( link, categoryMap );
-                    sortedMap.put( title, menuItem );
-                }
-            }
-            else
-            {
-                String title = ( String ) map.get( key );
-                String link = "<a href=\"" + appRoot + "/" + key + "\">" + 
title + "</a>";
-                if ( sortedMap.containsKey( title ) )
-                {
-                    ( ( MenuItem ) sortedMap.get( title ) ).setLink( link );
-                }
-                else
-                {
-                    MenuItem menuItem = new MenuItem( link );
-                    sortedMap.put( title, menuItem );
-                }
-            }
-
-        }
-        return sortedMap;
-    }
-
-     /**
-     * Returns the {@link RequestVariableResolver} for the given request.
-     *
-     * @param request The request whose attribute is returned 
-     */
-    protected RequestVariableResolver getVariableResolver( final 
ServletRequest request) {
-        return (RequestVariableResolver) request.getAttribute( 
RequestVariableResolver.REQUEST_ATTRIBUTE );
-    }
-
- 
-    @SuppressWarnings({ "rawtypes" })
-    private static class MenuItem
-    {
-    private String link;
-        private Map subMenu;
-
-        public MenuItem( String link )
-        {
-            this.link = link;
-        }
-
-        public MenuItem( String link, Map subMenu )
-        {
-            super();
-            this.link = link;
-            this.subMenu = subMenu;
-        }
-
-
-        public String getLink()
-        {
-            return link;
-        }
-
-
-        public void setLink( String link )
-        {
-            this.link = link;
-        }
-
-
-        public Map getSubMenu()
-        {
-            return subMenu;
-        }
-
-
-        public void setSubMenu( Map subMenu )
-        {
-            this.subMenu = subMenu;
-        }
-    }
-}
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractOsgiManagerPlugin.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractOsgiManagerPlugin.java
index d49675e018..af665f3f8a 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractOsgiManagerPlugin.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractOsgiManagerPlugin.java
@@ -41,9 +41,6 @@ public abstract class AbstractOsgiManagerPlugin extends 
AbstractServlet implemen
     /**
      * The name of the request attribute providing a mapping of labels to page
      * titles of registered console plugins (value is 
"felix.webconsole.labelMap").
-     * This map may be used to render a navigation of the console plugins as 
the
-     * {@link 
AbstractWebConsolePlugin#renderTopNavigation(javax.servlet.http.HttpServletRequest,
 java.io.PrintWriter)}
-     * method does.
      * <p>
      * The type of this request attribute is <code>Map&lt;String, 
String&gt;</code>.
      */
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractPluginAdapter.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractPluginAdapter.java
new file mode 100644
index 0000000000..063ca244fb
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractPluginAdapter.java
@@ -0,0 +1,651 @@
+/*
+ * 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.felix.webconsole.internal.servlet;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.felix.webconsole.servlet.RequestVariableResolver;
+import org.apache.felix.webconsole.servlet.ServletConstants;
+import org.apache.felix.webconsole.spi.BrandingPlugin;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+
+/**
+ * Base class for the servlet adapters
+ */
+public abstract class AbstractPluginAdapter extends HttpServlet {
+
+    public static final String ATTR_LANG_MAP = "felix.webconsole.langMap";
+
+    /** Pseudo class version ID to keep the IDE quite. */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * The header fragment read from the templates/main_header.html file
+     */
+    private static final String HEADER = readTemplateFile( 
"/templates/main_header.html" );
+
+    /**
+     * The footer fragment read from the templates/main_footer.html file
+     */
+    private static final String FOOTER = readTemplateFile( 
"/templates/main_footer.html" );
+
+    private static volatile BrandingPlugin BRANDING_PLUGIN = new 
BrandingPluginImpl();
+
+    private static volatile int LOGLEVEL;
+
+    private volatile BundleContext bundleContext;
+
+    private final String title;
+
+    protected final String label;
+
+    private final String[] cssReferences;
+
+    protected abstract URL getResource(final String pathInfo);
+
+    public AbstractPluginAdapter(final BundleContext bundleContext, final 
String label, final String title, final String[] cssReferences) {
+        this.title = title;
+        this.label = label;
+        this.cssReferences = cssReferences;
+        this.activate(bundleContext);
+    }
+
+    /**
+     * This method is used to render the content of the plug-in. It is called 
internally
+     * from the Web Console.
+     *
+     * @param req the HTTP request send from the user
+     * @param res the HTTP response object, where to render the plugin data.
+     * @throws IOException if an input or output error is
+     *  detected when the servlet handles the request
+     * @throws ServletException  if the request for the GET
+     *  could not be handled
+     */
+    protected abstract void renderContent(final HttpServletRequest req, final 
HttpServletResponse res )
+    throws ServletException, IOException;
+
+    //---------- HttpServlet Overwrites 
----------------------------------------
+
+    /**
+     * Returns the title for this plugin
+     */
+    @Override
+    public String getServletName() {
+        return this.title;
+    }
+
+    /**
+     * Renders the web console page for the request. This consist of the
+     * following five parts called in order:
+     * <ol>
+     * <li>Send back a requested resource
+     * <li>{@link #startResponse(HttpServletRequest, HttpServletResponse)}</li>
+     * <li>{@link #renderTopNavigation(HttpServletRequest, PrintWriter)}</li>
+     * <li>{@link #renderContent(HttpServletRequest, HttpServletResponse)}</li>
+     * <li>{@link #endResponse(PrintWriter)}</li>
+     * </ol>
+     * <p>
+     * <b>Note</b>: If a resource is sent back for the request only the first
+     * step is executed. Otherwise the first step is a null-operation actually
+     * and the latter four steps are executed in order.
+     * <p>
+     * If the {@link #isHtmlRequest(HttpServletRequest)} method returns
+     * <code>false</code> only the
+     * {@link #renderContent(HttpServletRequest, HttpServletResponse)} method 
is
+     * called.
+     */
+    protected void doGet(final HttpServletRequest request, final 
HttpServletResponse response )
+    throws ServletException, IOException {
+        if ( !spoolResource( request, response ) ) {
+            // detect if this is an html request
+            if ( isHtmlRequest(request) ) {
+                // start the html response, write the header, open body and 
main div
+                PrintWriter pw = startResponse( request, response );
+
+                // render top navigation
+                renderTopNavigation( request, pw );
+
+                // wrap content in a separate div
+                pw.println( "<div id='content'>" );
+                renderContent( request, response );
+                pw.println( "</div>" );
+
+                // close the main div, body, and html
+                endResponse( pw );
+            } else {
+                renderContent( request, response );
+            }
+        }
+    }
+
+    /**
+     * Detects whether this request is intended to have the headers and
+     * footers of this plugin be rendered or not. This method always returns
+     * <code>true</code> and may be overwritten by plugins to detect
+     * from the actual request, whether or not to render the header and
+     * footer.
+     *
+     * @param request the original request passed from the HTTP server
+     * @return <code>true</code> if the page should have headers and footers 
rendered
+     */
+    protected boolean isHtmlRequest( final HttpServletRequest request ) {
+        return true;
+    }
+    /**
+     * This method is called from the Felix Web Console to ensure the
+     * AbstractWebConsolePlugin is correctly setup.
+     *
+     * It is called right after the Web Console receives notification for
+     * plugin registration.
+     *
+     * @param bundleContext the context of the plugin bundle
+     */
+    protected void activate( BundleContext bundleContext ) {
+        this.bundleContext = bundleContext;
+    }
+
+    /**
+     * This method is called, by the Web Console to de-activate the plugin and 
release
+     * all used resources.
+     */
+    public void deactivate() {
+        this.bundleContext = null;
+    }
+
+    /**
+     * Returns the <code>BundleContext</code> with which this plugin has been
+     * activated. If the plugin has not be activated by calling the
+     * {@link #activate(BundleContext)} method, this method returns
+     * <code>null</code>.
+     *
+     * @return the bundle context or <code>null</code> if the bundle is not 
activated.
+     */
+    protected BundleContext getBundleContext() {
+        return bundleContext;
+    }
+
+    /**
+     * Returns the <code>Bundle</code> pertaining to the
+     * {@link #getBundleContext() bundle context} with which this plugin has
+     * been activated. If the plugin has not be activated by calling the
+     * {@link #activate(BundleContext)} method, this method returns
+     * <code>null</code>.
+     *
+     * @return the bundle or <code>null</code> if the plugin is not activated.
+     */
+    private final Bundle getBundle() {
+        final BundleContext bundleContext = getBundleContext();
+        return ( bundleContext != null ) ? bundleContext.getBundle() : null;
+    }
+
+    /**
+     * Calls the <code>ServletContext.log(String)</code> method if the
+     * configured log level is less than or equal to the given 
<code>level</code>.
+     * <p>
+     * Note, that the <code>level</code> paramter is only used to decide 
whether
+     * the <code>GenericServlet.log(String)</code> method is called or not. The
+     * actual implementation of the <code>GenericServlet.log</code> method is
+     * outside of the control of this method.
+     * <p>
+     * If the servlet has not been initialized yet or has already been 
destroyed
+     * the message is printed to stderr.
+     *
+     * @param level The log level at which to log the message
+     * @param message The message to log
+     */
+    public void log(final int level, final String message ) {
+        this.log(level, message, null);
+    }
+
+    /**
+     * Calls the <code>ServletContext.log(String, Throwable)</code> method if
+     * the configured log level is less than or equal to the given
+     * <code>level</code>.
+     * <p>
+     * Note, that the <code>level</code> paramter is only used to decide 
whether
+     * the <code>GenericServlet.log(String, Throwable)</code> method is called
+     * or not. The actual implementation of the <code>GenericServlet.log</code>
+     * method is outside of the control of this method.
+     *
+     * @param level The log level at which to log the message
+     * @param message The message to log
+     * @param t The <code>Throwable</code> to log with the message
+     */
+    public void log(final int level, final String message, final Throwable t ) 
{
+        if ( LOGLEVEL >= level ) {
+            final ServletConfig config = getServletConfig();
+            if ( config != null ) {
+                final ServletContext context = config.getServletContext();
+                if ( context != null ) {
+                    if ( t != null ) {
+                        context.log( message, t );
+                    } else {
+                        context.log( message );
+                    }
+                    return;
+                }
+            }
+
+            System.err.println( message );
+            if ( t != null ) {
+                t.printStackTrace( System.err );
+            }
+        }
+    }
+    
+    /**
+     * Spool the resource
+     * @throws IOException If an error occurs accessing or spooling the 
resource.
+     */
+    private final boolean spoolResource(final HttpServletRequest request,  
final HttpServletResponse response)
+    throws IOException {
+        final String pi = request.getPathInfo();
+        final URL url = this.getResource(pi);
+        if (url == null) {
+            return false;
+        }
+
+        // open the connection and the stream (we use the stream to be able
+        // to at least hint to close the connection because there is no
+        // method to explicitly close the conneciton, unfortunately)
+        URLConnection connection = url.openConnection();
+        try ( InputStream ins = connection.getInputStream()) {
+            // FELIX-2017 Equinox may return an URL for a non-existing
+            // resource but then (instead of throwing) return null on
+            // getInputStream. We should account for this situation and
+            // just assume a non-existing resource in this case.
+            if (ins == null) {
+                return false;
+            }
+
+            // check whether we may return 304/UNMODIFIED
+            final long lastModified = connection.getLastModified();
+            if ( lastModified > 0 ) {
+                long ifModifiedSince = request.getDateHeader( 
"If-Modified-Since" );
+                if ( ifModifiedSince >= ( lastModified / 1000 * 1000 ) ) {
+                    // Round down to the nearest second for a proper compare
+                    // A ifModifiedSince of -1 will always be less
+                    response.setStatus( HttpServletResponse.SC_NOT_MODIFIED );
+
+                    return true;
+                }
+
+                // have to send, so set the last modified header now
+                response.setDateHeader( "Last-Modified", lastModified );
+            }
+
+            // describe the contents
+            response.setContentType( getServletContext().getMimeType( pi ) );
+            response.setIntHeader( "Content-Length", 
connection.getContentLength() );
+
+            // spool the actual contents
+            final OutputStream out = response.getOutputStream();
+            final byte[] buf = new byte[2048];
+            int rd;
+            while ( ( rd = ins.read( buf ) ) >= 0 ) {
+                out.write( buf, 0, rd );
+            }
+
+            return true;
+        }
+    }
+
+    /**
+     * This method is responsible for generating the top heading of the page.
+     *
+     * @param request the HTTP request coming from the user
+     * @param response the HTTP response, where data is rendered
+     * @return the writer that was used for generating the response.
+     * @throws IOException on I/O error
+     * @see #endResponse(PrintWriter)
+     */
+    private PrintWriter startResponse(final HttpServletRequest request, final 
HttpServletResponse response ) throws IOException {
+        response.setCharacterEncoding( "utf-8" );
+        response.setContentType( "text/html" );
+
+        final PrintWriter pw = response.getWriter();
+
+        final String appRoot = ( String ) request.getAttribute( 
ServletConstants.ATTR_APP_ROOT );
+
+        // support localization of the plugin title
+        String t= this.title;
+        if ( t.startsWith( "%" ) ) {
+            t = "${" + t.substring( 1 ) + "}";
+        }
+
+        final RequestVariableResolver r = this.getVariableResolver(request);
+        r.put("head.title", t);
+        r.put("head.label", this.label);
+        r.put("head.cssLinks", getCssLinks(appRoot));
+        r.put("brand.name", BRANDING_PLUGIN.getBrandName());
+        r.put("brand.product.url", BRANDING_PLUGIN.getProductURL());
+        r.put("brand.product.name", BRANDING_PLUGIN.getProductName());
+        r.put("brand.product.img", toUrl( BRANDING_PLUGIN.getProductImage(), 
appRoot ));
+        r.put("brand.favicon", toUrl( BRANDING_PLUGIN.getFavIcon(), appRoot ));
+        r.put("brand.css", toUrl( BRANDING_PLUGIN.getMainStyleSheet(), appRoot 
));
+        pw.println( HEADER );
+
+        return pw;
+    }
+
+    /**
+     * This method is called to generate the top level links with the 
available plug-ins.
+     *
+     * @param request the HTTP request coming from the user
+     * @param pw the writer, where the HTML data is rendered
+     */
+    @SuppressWarnings({ "rawtypes" })
+    private void renderTopNavigation(final HttpServletRequest request, final 
PrintWriter pw ) {
+        // assume pathInfo to not be null, else this would not be called
+        String current = request.getPathInfo();
+        int slash = current.indexOf( "/", 1 );
+        if ( slash < 0 ) {
+            slash = current.length();
+        }
+        current = current.substring( 1, slash );
+
+        final String appRoot = ( String ) request.getAttribute( 
ServletConstants.ATTR_APP_ROOT );
+
+        @SuppressWarnings("deprecation")
+        final Map menuMap = ( Map ) request.getAttribute( 
OsgiManager.ATTR_LABEL_MAP_CATEGORIZED );
+        this.renderMenu( menuMap, appRoot, pw );
+
+        // render lang-box
+        Map langMap = (Map) request.getAttribute(ATTR_LANG_MAP);
+        if (null != langMap && !langMap.isEmpty()) {
+            // determine the currently selected locale from the request and 
fail-back
+            // to the default locale if not set
+            // if locale is missing in locale map, the default 'en' locale is 
used
+            Locale reqLocale = request.getLocale();
+            String locale = null != reqLocale ? reqLocale.getLanguage()
+                : Locale.getDefault().getLanguage();
+            if (!langMap.containsKey(locale)) {
+                locale = Locale.getDefault().getLanguage();
+            }
+            if (!langMap.containsKey(locale)) {
+                locale = "en";
+            }
+
+            pw.println("<div id='langSelect'>");
+            pw.println(" <span>");
+            printLocaleElement(pw, appRoot, locale, langMap.get(locale));
+            pw.println(" </span>");
+            pw.println(" <span class='flags ui-helper-hidden'>");
+            for (Iterator li = langMap.keySet().iterator(); li.hasNext();) {
+                // <img src="us.gif" alt="en" title="English"/>
+                final Object l = li.next();
+                if (!l.equals(locale)) {
+                    printLocaleElement(pw, appRoot, l, langMap.get(l));
+                }
+            }
+
+            pw.println(" </span>");
+            pw.println("</div>");
+        }
+    }
+
+    @SuppressWarnings({ "rawtypes" })
+    private void renderMenu(final Map menuMap, final String appRoot, final 
PrintWriter pw ) {
+        if ( menuMap != null ) {
+            final SortedMap categoryMap = sortMenuCategoryMap( menuMap, 
appRoot );
+            pw.println( "<ul id=\"navmenu\">" );
+            renderSubmenu( categoryMap, appRoot, pw, 0 );
+            pw.println("<li class=\"logoutButton navMenuItem-0\">");
+            pw.println("<a href=\"" + appRoot + "/logout\">${logout}</a>");
+            pw.println("</li>");
+            pw.println( "</ul>" );
+        }
+    }
+
+    @SuppressWarnings({ "rawtypes" })
+    private void renderMenu(final Map menuMap, final String appRoot, final 
PrintWriter pw, final int level ) {
+        pw.println( "<ul class=\"navMenuLevel-" + level + "\">" );
+        renderSubmenu( menuMap, appRoot, pw, level );
+        pw.println( "</ul>" );
+    }
+
+    @SuppressWarnings({ "rawtypes" })
+    private void renderSubmenu(final Map menuMap, final String appRoot, final 
PrintWriter pw, final int level ) {
+        String liStyleClass = " class=\"navMenuItem-" + level + "\"";
+        Iterator itr = menuMap.keySet().iterator();
+        while ( itr.hasNext() )
+        {
+            String key = ( String ) itr.next();
+            MenuItem menuItem = ( MenuItem ) menuMap.get( key );
+            pw.println( "<li" + liStyleClass + ">" + menuItem.getLink() );
+            Map subMenu = menuItem.getSubMenu();
+            if ( subMenu != null )
+            {
+                renderMenu( subMenu, appRoot, pw, level + 1 );
+            }
+            pw.println( "</li>" );
+        }
+    }
+
+    private static final void printLocaleElement( PrintWriter pw, String 
appRoot, Object langCode, Object langName ) {
+        pw.print("  <img src='");
+        pw.print(appRoot);
+        pw.print("/res/flags/");
+        pw.print(langCode);
+        pw.print(".gif' alt='");
+        pw.print(langCode);
+        pw.print("' title='");
+        pw.print(langName);
+        pw.println("'/>");
+    }
+
+    /**
+     * This method is responsible for generating the footer of the page.
+     *
+     * @param pw the writer, where the HTML data is rendered
+     * @see #startResponse(HttpServletRequest, HttpServletResponse)
+     */
+    private void endResponse( PrintWriter pw ) {
+        pw.println(FOOTER);
+    }
+
+    /**
+     * Sets the {@link BrandingPlugin} to use globally by all extensions of
+     * this class for branding.
+     * <p>
+     * Note: This method is intended to be used internally by the Web Console
+     * to update the branding plugin to use.
+     *
+     * @param brandingPlugin the brandingPlugin to set
+     */
+    public static final void setBrandingPlugin(final BrandingPlugin 
brandingPlugin) {
+        if (brandingPlugin == null){
+            AbstractPluginAdapter.BRANDING_PLUGIN = new BrandingPluginImpl();
+        } else {
+            AbstractPluginAdapter.BRANDING_PLUGIN = brandingPlugin;
+        }
+    }
+
+    /**
+     * Sets the log level to be applied for calls to the {@link #log(int, 
String)}
+     * and {@link #log(int, String, Throwable)} methods.
+     * <p>
+     * Note: This method is intended to be used internally by the Web Console
+     * to update the log level according to the Web Console configuration.
+     *
+     * @param logLevel the maximum allowed log level. If message is logged with
+     *        lower level it will not be forwarded to the logger.
+     */
+    public static final void setLogLevel( final int logLevel ) {
+        AbstractPluginAdapter.LOGLEVEL = logLevel;
+    }
+
+    private static final String readTemplateFile( final String templateFile) {
+        try(final InputStream templateStream = 
AbstractPluginAdapter.class.getResourceAsStream( templateFile )) {
+            if ( templateStream != null ) {
+                try ( final StringWriter w = new StringWriter()) {
+                    final byte[] buf = new byte[2048];
+                    int l;
+                    while ( ( l = templateStream.read(buf)) > 0 ) {
+                        w.write(new String(buf, 0, l, StandardCharsets.UTF_8));
+                    }
+                    String str = w.toString();
+                    switch ( str.charAt(0) )
+                    { // skip BOM
+                        case 0xFEFF: // UTF-16/UTF-32, big-endian
+                        case 0xFFFE: // UTF-16, little-endian
+                        case 0xEFBB: // UTF-8
+                            return str.substring(1);
+                    }
+                    return str;
+                }
+            }
+        } catch (final IOException e ) {
+            // don't use new Exception(message, cause) because cause is 1.4+
+            throw new RuntimeException( "readTemplateFile: Error loading " + 
templateFile + ": " + e );
+        }
+
+        // template file does not exist, throw
+        throw new RuntimeException("readTemplateFile: File '" + templateFile + 
"' not found in webconsole bundle");
+    }
+
+    private final String getCssLinks( final String appRoot ) {
+        // get the CSS references and return nothing if there are none
+        if ( this.cssReferences == null || this.cssReferences.length == 0) {
+            return "";
+        }
+
+        // build the CSS links from the references
+        final StringBuilder buf = new StringBuilder();
+        for ( int i = 0; i < this.cssReferences.length; i++ ) {
+            buf.append( "<link href='" );
+            buf.append( toUrl( this.cssReferences[i], appRoot ) );
+            buf.append( "' rel='stylesheet' type='text/css' />" );
+        }
+
+        return buf.toString();
+    }
+
+    /**
+     * If the <code>url</code> starts with a slash, it is considered an 
absolute
+     * path (relative URL) which must be prefixed with the Web Console
+     * application root path. Otherwise the <code>url</code> is assumed to
+     * either be a relative path or an absolute URL, both must not be prefixed.
+     *
+     * @param url The url path to optionally prefix with the application root
+     *          path
+     * @param appRoot The application root path to optionally put in front of
+     *          the url.
+     * @throws NullPointerException if <code>url</code> is <code>null</code>.
+     */
+    private static final String toUrl( final String url, final String appRoot 
) {
+        if ( url.startsWith( "/" ) ) {
+            return appRoot + url;
+        }
+        return url;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private SortedMap sortMenuCategoryMap(final Map map, final String appRoot 
) {
+        final SortedMap sortedMap = new TreeMap<>( 
String.CASE_INSENSITIVE_ORDER );
+        final Iterator keys = map.keySet().iterator();
+        while ( keys.hasNext() ) {
+            final String key = ( String ) keys.next();
+            if ( key.startsWith( "category." ) ) {
+                final SortedMap categoryMap = sortMenuCategoryMap( ( Map ) 
map.get( key ), appRoot );
+                final String title = key.substring( key.indexOf( '.' ) + 1 );
+                if ( sortedMap.containsKey( title ) ) {
+                    ( ( MenuItem ) sortedMap.get( title ) ).setSubMenu( 
categoryMap );
+                } else {
+                    final String link = "<a href=\"#\">" + title + "</a>";
+                    final MenuItem menuItem = new MenuItem( link, categoryMap 
);
+                    sortedMap.put( title, menuItem );
+                }
+            } else {
+                final String title = ( String ) map.get( key );
+                final String link = "<a href=\"" + appRoot + "/" + key + "\">" 
+ title + "</a>";
+                if ( sortedMap.containsKey( title ) ) {
+                    ( ( MenuItem ) sortedMap.get( title ) ).setLink( link );
+                } else {
+                    final MenuItem menuItem = new MenuItem( link );
+                    sortedMap.put( title, menuItem );
+                }
+            }
+        }
+        return sortedMap;
+    }
+
+    /**
+     * Get the variable resolver
+     * @param request The request
+     * @return The resolver
+     */
+    protected RequestVariableResolver getVariableResolver( final 
ServletRequest request) {
+        return (RequestVariableResolver) request.getAttribute( 
RequestVariableResolver.REQUEST_ATTRIBUTE );
+    }
+
+    @SuppressWarnings({ "rawtypes" })
+    private static class MenuItem {
+
+        private String link;
+        private Map subMenu;
+
+        public MenuItem(final String link ) {
+            this.link = link;
+        }
+
+        public MenuItem(final String link, final Map subMenu ) {
+            this.link = link;
+            this.subMenu = subMenu;
+        }
+
+        public String getLink() {
+            return link;
+        }
+
+
+        public void setLink(final String link ) {
+            this.link = link;
+        }
+
+
+        public Map getSubMenu() {
+            return subMenu;
+        }
+
+
+        public void setSubMenu(final Map subMenu ) {
+            this.subMenu = subMenu;
+        }
+    }
+}
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java
index 15f92ce9e9..99b859c20b 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProvider.java
@@ -47,7 +47,9 @@ public class BasicWebConsoleSecurityProvider implements 
SecurityProvider {
     private final BundleContext bundleContext;
 
     public BasicWebConsoleSecurityProvider(final BundleContext bundleContext) {
-        this(bundleContext, null, null);
+        this.bundleContext = bundleContext;
+        this.username = null;
+        this.password = null;
     }
 
     public BasicWebConsoleSecurityProvider(final BundleContext bundleContext, 
final String username, final String password) {
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationUtil.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationUtil.java
index ad3cc8c71c..8198a341d3 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationUtil.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/ConfigurationUtil.java
@@ -150,6 +150,7 @@ public class ConfigurationUtil
      * @param name The name of the property to return
      * @return the property value as string array - no matter if originally it 
was other kind of array, collection or comma-separated string. Returns 
<code>null</code> if the property is not set.
      */
+    @SuppressWarnings("rawtypes")
     public static final String[] getStringArrayProperty(Map<String, Object> 
config, String name)
     {
         Object value = config.get(name);
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/JakartaServletAdapter.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/EnhancedPluginAdapter.java
similarity index 65%
rename from 
webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/JakartaServletAdapter.java
rename to 
webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/EnhancedPluginAdapter.java
index 21cf7ab797..5f5adb67a7 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/JakartaServletAdapter.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/EnhancedPluginAdapter.java
@@ -19,12 +19,8 @@
 package org.apache.felix.webconsole.internal.servlet;
 
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
-
+import java.net.URL;
 import org.apache.felix.webconsole.servlet.AbstractServlet;
-import org.apache.felix.webconsole.servlet.ServletConstants;
 import org.osgi.framework.ServiceReference;
 
 import jakarta.servlet.Servlet;
@@ -35,42 +31,32 @@ import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.HttpServletResponseWrapper;
 
 /**
- * The <code>JakartaServletAdapter</code> is an adapter to the
- * {@link AbstractWebConsolePlugin} for regular servlets registered with the
- * {@link org.apache.felix.webconsole.WebConsoleConstants#PLUGIN_TITLE}
- * service attribute using jakarta.servlet.Servlet
+ * The <code>SimplePluginAdapter</code> is an adapter to the
+ * {@link AbstractPluginAdapter} for servlets extending
+ * {@link AbstractServlet}.
  */
-public class JakartaServletAdapter extends AbstractInternalPlugin {
+public class EnhancedPluginAdapter extends AbstractPluginAdapter {
 
     /** serial UID */
     private static final long serialVersionUID = 1L;
 
-    // the actual plugin to forward rendering requests to
+    /** The actual plugin to forward rendering requests to */
     private final AbstractServlet plugin;
 
     /**
      * Creates a new wrapper for a Web Console Plugin
-     *
-     * @param plugin the plugin itself
-     * @param serviceReference reference to the plugin
      */
-    public JakartaServletAdapter( final AbstractServlet plugin, 
ServiceReference<Servlet> serviceReference ) {
-        
super((String)serviceReference.getProperty(ServletConstants.PLUGIN_LABEL), 
toStringArray( serviceReference.getProperty( 
ServletConstants.PLUGIN_CSS_REFERENCES ) ));
+    public EnhancedPluginAdapter(final AbstractServlet plugin, 
+            final ServiceReference<Servlet> serviceReference, 
+            final String label, final String title, final String[] 
cssReferences) {
+        super(serviceReference.getBundle().getBundleContext(), label, title, 
cssReferences);
         this.plugin = plugin;
-
-        // activate this abstract plugin (mainly to set the bundle context)
-        activate( serviceReference.getBundle().getBundleContext() );
     }
 
-    /**
-     * Returns the registered plugin class to be able to call the
-     * <code>getResource()</code> method on that object for this plugin to
-     * provide additional resources.
-     */
-    protected Object getResourceProvider() {
-        return plugin;
+    @Override
+    protected URL getResource(final String path) {
+        return null;
     }
-
     /** 
      * Initialize the plugin
      */
@@ -134,10 +120,10 @@ public class JakartaServletAdapter extends 
AbstractInternalPlugin {
             super.setStatus(sc);
         }
 
-        @Override
         public void setStatus(final int sc, final String sm) {
             this.done = true;
-            super.setStatus(sc, sm);
+            // use non deprecated method (servlet api 6)
+            super.setStatus(sc);
         }
 
         @Override
@@ -184,45 +170,4 @@ public class JakartaServletAdapter extends 
AbstractInternalPlugin {
             deactivate();
         }
     }
-
-
-    //---------- internal
-
-    @SuppressWarnings("rawtypes")
-    private static String[] toStringArray( final Object value ) {
-        if ( value instanceof String )
-        {
-            return new String[]
-                { ( String ) value };
-        }
-        else if ( value != null )
-        {
-            final Collection cssListColl;
-            if ( value.getClass().isArray() )
-            {
-                cssListColl = Arrays.asList( ( Object[] ) value );
-            }
-            else if ( value instanceof Collection )
-            {
-                cssListColl = ( Collection ) value;
-            }
-            else
-            {
-                cssListColl = null;
-            }
-
-            if ( cssListColl != null && !cssListColl.isEmpty() )
-            {
-                String[] entries = new String[cssListColl.size()];
-                int i = 0;
-                for ( Iterator cli = cssListColl.iterator(); cli.hasNext(); 
i++ )
-                {
-                    entries[i] = String.valueOf( cli.next() );
-                }
-                return entries;
-            }
-        }
-
-        return null;
-    }
 }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 6d1e3cd0d9..bf48602cde 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -39,8 +39,6 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentSkipListSet;
 
-import org.apache.felix.webconsole.AbstractWebConsolePlugin;
-import org.apache.felix.webconsole.DefaultVariableResolver;
 import org.apache.felix.webconsole.servlet.User;
 import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
 import org.apache.felix.webconsole.internal.Util;
@@ -589,7 +587,7 @@ public class OsgiManager extends HttpServlet {
         request.setAttribute(ATTR_APP_ROOT_OLD, appRoot);
 
         @SuppressWarnings("deprecation")
-        final RequestVariableResolver resolver = new DefaultVariableResolver();
+        final RequestVariableResolver resolver = new 
org.apache.felix.webconsole.DefaultVariableResolver();
         request.setAttribute(RequestVariableResolver.REQUEST_ATTRIBUTE, 
resolver);
         resolver.put( RequestVariableResolver.KEY_APP_ROOT, (String) 
request.getAttribute( ServletConstants.ATTR_APP_ROOT ) );
         resolver.put( RequestVariableResolver.KEY_PLUGIN_ROOT, (String) 
request.getAttribute( ServletConstants.ATTR_PLUGIN_ROOT ) );
@@ -731,23 +729,8 @@ public class OsgiManager extends HttpServlet {
      * @param level The log level at which to log the message
      * @param message The message to log
      */
-    void log(int level, String message)
-    {
-        if (logLevel >= level)
-        {
-            ServletConfig config = getServletConfig();
-            if ( config != null )
-            {
-                ServletContext context = config.getServletContext();
-                if ( context != null )
-                {
-                    context.log( message );
-                    return;
-                }
-            }
-
-            System.err.println( message );
-        }
+    public void log(final int level, final String message) {
+        this.log(level, message, null);
     }
 
     /**
@@ -767,37 +750,30 @@ public class OsgiManager extends HttpServlet {
      * @param message The message to log
      * @param t The <code>Throwable</code> to log with the message
      */
-    void log(int level, String message, Throwable t)
-    {
-        if (logLevel >= level)
-        {
-            ServletConfig config = getServletConfig();
-            if ( config != null )
-            {
-                ServletContext context = config.getServletContext();
-                if ( context != null )
-                {
-                    context.log( message, t );
+    public void log(final int level, final String message, final Throwable t) {
+        if (logLevel >= level) {
+            final ServletConfig config = getServletConfig();
+            if ( config != null ) {
+                final ServletContext context = config.getServletContext();
+                if ( context != null ) {
+                    if (t != null) {
+                        context.log( message, t );
+                    } else {
+                        context.log( message );
+                    }
                     return;
                 }
             }
 
             System.err.println( message );
-            if ( t != null )
-            {
+            if ( t != null ) {
                 t.printStackTrace( System.err );
             }
         }
     }
 
-    private HttpServletRequest wrapRequest(final HttpServletRequest request,
-        final Locale locale)
-    {
-        return new HttpServletRequestWrapper(request)
-        {
-            /**
-             * @see javax.servlet.ServletRequestWrapper#getLocale()
-             */
+    private HttpServletRequest wrapRequest(final HttpServletRequest request, 
final Locale locale) {
+        return new HttpServletRequestWrapper(request)  {
             @Override
             public Locale getLocale()
             {
@@ -836,17 +812,19 @@ public class OsgiManager extends HttpServlet {
             final BrandingPlugin plugin = super.addingService(reference);
             if (plugin != null) {
                 if (plugin instanceof 
org.apache.felix.webconsole.BrandingPlugin) {
-                    
AbstractWebConsolePlugin.setBrandingPlugin((org.apache.felix.webconsole.BrandingPlugin)plugin);
+                    
org.apache.felix.webconsole.AbstractWebConsolePlugin.setBrandingPlugin((org.apache.felix.webconsole.BrandingPlugin)plugin);
                 } else {
-                    AbstractWebConsolePlugin.setBrandingPlugin(new 
BrandingPluginAdapter(plugin));
+                    
org.apache.felix.webconsole.AbstractWebConsolePlugin.setBrandingPlugin(new 
BrandingPluginAdapter(plugin));
                 }
+                AbstractPluginAdapter.setBrandingPlugin(plugin);
             }
             return plugin;
         }
 
         @Override
         public void removedService(final ServiceReference<BrandingPlugin> 
reference, BrandingPlugin service) {
-            AbstractWebConsolePlugin.setBrandingPlugin(null);
+            
org.apache.felix.webconsole.AbstractWebConsolePlugin.setBrandingPlugin(null);
+            AbstractPluginAdapter.setBrandingPlugin(null);
             try {
                 super.removedService(reference, service);
             } catch ( final IllegalStateException ise) {
@@ -1020,8 +998,9 @@ public class OsgiManager extends HttpServlet {
 
         this.logLevel = ConfigurationUtil.getProperty(config, PROP_LOG_LEVEL, 
DEFAULT_LOG_LEVEL);
         AbstractOsgiManagerPlugin.LOGLEVEL = logLevel;
-        AbstractWebConsolePlugin.setLogLevel(logLevel);
-    
+        
org.apache.felix.webconsole.AbstractWebConsolePlugin.setLogLevel(logLevel);
+        AbstractPluginAdapter.setLogLevel(logLevel);
+
         // default plugin page configuration
         holder.setDefaultPluginLabel(ConfigurationUtil.getProperty(config, 
PROP_DEFAULT_RENDER, DEFAULT_PAGE));
 
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
index a6b896ba8f..bbf104baa4 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContext.java
@@ -51,6 +51,7 @@ final class OsgiManagerHttpContext extends 
ServletContextHelper {
     }
 
     @Override
+    @SuppressWarnings("deprecation")
     public boolean handleSecurity( final HttpServletRequest r, final 
HttpServletResponse response ) {
         final SecurityProvider provider = tracker.getService();
 
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java
index 7011b926ad..1cb5cab155 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Password.java
@@ -34,8 +34,7 @@ import java.security.SecureRandom;
  * the password and <i>password</i> is the password
  * hashed with the indicated hash algorithm.
  */
-class Password
-{
+class Password {
 
     // the default hash algorithm (part of the Java Platform since 1.4)
     private static final String DEFAULT_HASH_ALGO = "SHA-256";
@@ -55,7 +54,6 @@ class Password
     // the hashed or plain password
     private final String password;
 
-
     /**
      * Returns {@code true} if the given {@code textPassword} is hashed
      * and encoded as described in the class comment.
@@ -64,12 +62,10 @@ class Password
      * @return
      * @throws NullPointerException if {@code textPassword} is {@code null}.
      */
-    static boolean isPasswordHashed( final String textPassword )
-    {
+    static boolean isPasswordHashed( final String textPassword ) {
         return getEndOfHashAlgorithm( textPassword ) >= 0;
     }
 
-
     /**
      * Returns the given plain {@code textPassword} as an encoded hashed
      * password string as described in the class comment.
@@ -78,14 +74,12 @@ class Password
      * @return
      * @throws NullPointerException if {@code textPassword} is {@code null}.
      */
-    static String hashPassword( final String textPassword )
-    {
+    static String hashPassword( final String textPassword ) {
         String salt = generateSalt(DEFAULT_SALT_SIZE);
         return hashPassword( DEFAULT_HASH_ALGO, DEFAULT_ITERATIONS, salt, 
textPassword  );
     }
 
-    Password( String textPassword )
-    {
+    Password( final String textPassword ) {
         this.hashAlgo = getPasswordHashAlgorithm( textPassword );
         this.password = getPassword(textPassword);
     }
@@ -100,15 +94,12 @@ class Password
      * @return
      * @throws NullPointerException if {@code toCompare} is {@code null}.
      */
-    boolean matches( final byte[] toCompare )
-    {
-        if (this.hashAlgo != null) 
-        {
+    boolean matches( final byte[] toCompare ) {
+        if (this.hashAlgo != null)  {
             int startPos = 0;
             String salt = extractSalt(this.password, startPos);
             int iterations = NO_ITERATIONS;
-            if (salt != null) 
-            {
+            if (salt != null) {
                 startPos += salt.length()+1;
                 iterations = extractIterations(this.password, startPos);
                
@@ -122,8 +113,7 @@ class Password
         
     }
     
-    private static String hashPassword( final String hashAlgorithm, final int 
iterations, final  String salt, final String password )
-    {
+    private static String hashPassword( final String hashAlgorithm, final int 
iterations, final  String salt, final String password ) {
         
         final StringBuilder buf = new StringBuilder();
         buf.append( '{' ).append( hashAlgorithm.toLowerCase() ).append( '}' );
@@ -143,11 +133,9 @@ class Password
         return buf.toString();
     }
 
-    private static String getPasswordHashAlgorithm( final String textPassword )
-    {
+    private static String getPasswordHashAlgorithm( final String textPassword 
) {
         final int endHash = getEndOfHashAlgorithm( textPassword );
-        if ( endHash >= 0 )
-        {
+        if ( endHash >= 0 ) {
             return textPassword.substring( 1, endHash );
         }
 
@@ -155,11 +143,9 @@ class Password
         return null;
     }
 
-    private static String getPassword( final String textPassword )
-    {
+    private static String getPassword( final String textPassword ) {
         final int endHash = getEndOfHashAlgorithm( textPassword );
-        if ( endHash >= 0 )
-        {
+        if ( endHash >= 0 ) {
             final String encodedPassword = textPassword.substring( endHash + 1 
);
             return  encodedPassword;
         }
@@ -168,13 +154,10 @@ class Password
     }
 
 
-    private static int getEndOfHashAlgorithm( final String textPassword )
-    {
-        if ( textPassword.startsWith( "{" ) )
-        {
+    private static int getEndOfHashAlgorithm( final String textPassword ) {
+        if ( textPassword.startsWith( "{" ) ) {
             final int endHash = textPassword.indexOf( "}" );
-            if ( endHash > 0 )
-            {
+            if ( endHash > 0 ) {
                 return endHash;
             }
         }
@@ -182,82 +165,63 @@ class Password
         return -1;
     }
 
-    private static byte[] hashPassword( final String pwd, final String salt, 
final int iterations, final String hashAlg )
-    {
-        try
-        {
-            StringBuilder data = new StringBuilder();
-            if (salt != null) 
-            {
+    private static byte[] hashPassword( final String pwd, final String salt, 
final int iterations, final String hashAlg ) {
+        try {
+            final StringBuilder data = new StringBuilder();
+            if (salt != null) {
                 data.append(salt);
             }
             data.append(pwd);
             byte[] bytes =  Base64.getBytesUtf8( data.toString());
             final MessageDigest md = MessageDigest.getInstance( hashAlg );
-            for (int i = 0; i < iterations; i++) 
-            {
+            for (int i = 0; i < iterations; i++) {
                 md.reset();
                 bytes = md.digest(bytes);
             }
             return bytes;
-        }
-        catch ( NoSuchAlgorithmException e )
-        {
+        } catch ( final NoSuchAlgorithmException e ) {
             throw new IllegalStateException( "Cannot hash the password: " + e 
);
         }
     }
     
-    private static boolean compareSecure( final String a,final String b ) 
-    {
+    private static boolean compareSecure( final String a, final String b ) {
         int len = a.length();
-        if (len != b.length()) 
-        {
+        if (len != b.length()) {
             return false;
         }
-        if (len == 0) 
-        {
+        if (len == 0) {
             return true;
         }
         // don't use conditional operations inside the loop
         int bits = 0;
-        for (int i = 0; i < len; i++) 
-        {
+        for (int i = 0; i < len; i++) {
             // this will never reset any bits
             bits |= a.charAt(i) ^ b.charAt(i);
         }
         return bits == 0;
     }
     
-    private static String generateSalt( final  int saltSize ) 
-    {
-        SecureRandom random = new SecureRandom();
-        byte[] salt = new byte[saltSize];
+    private static String generateSalt( final  int saltSize ) {
+        final SecureRandom random = new SecureRandom();
+        final byte[] salt = new byte[saltSize];
         random.nextBytes(salt);
         return toHex(salt);
     }
     
-    private static String toHex( final byte[] array )
-    {
-        BigInteger bi = new BigInteger(1, array);
-        String hex = bi.toString(16);
-        int paddingLength = (array.length * 2) - hex.length();
-        if(paddingLength > 0) 
-        {
+    private static String toHex( final byte[] array ) {
+        final BigInteger bi = new BigInteger(1, array);
+        final String hex = bi.toString(16);
+        final int paddingLength = (array.length * 2) - hex.length();
+        if (paddingLength > 0) {
             return String.format("%0" + paddingLength + "d", 0) + hex;
-        }
-        else 
-        {
-            return hex;
-        }
+        } 
+        return hex;
     }
     
-    private static String extractSalt( final String hashedPwd, final int start 
) 
-    {
-        if (hashedPwd != null) 
-        {
-            int end = hashedPwd.indexOf(DELIMITER, start);
-            if (end > -1) 
-            {
+    private static String extractSalt( final String hashedPwd, final int start 
) {
+        if (hashedPwd != null) {
+            final int end = hashedPwd.indexOf(DELIMITER, start);
+            if (end > -1) {
                 return hashedPwd.substring(start, end);
             }
         }
@@ -265,19 +229,14 @@ class Password
         return null;
     }
     
-    private static int extractIterations( final String hashedPwd, int start ) 
-    {
-        if (hashedPwd != null) 
-        {
-            int end = hashedPwd.indexOf(DELIMITER, start);
-            if (end > -1) 
-            {
-                String str = hashedPwd.substring(start, end);
-                try 
-                {
+    private static int extractIterations( final String hashedPwd, final int 
start ) {
+        if (hashedPwd != null)  {
+            final int end = hashedPwd.indexOf(DELIMITER, start);
+            if (end > -1) {
+                final String str = hashedPwd.substring(start, end);
+                try {
                     return Integer.parseInt(str);
-                } catch (NumberFormatException e) 
-                {
+                } catch (NumberFormatException e) {
                     //nothing to do
                 }
             }
@@ -285,5 +244,4 @@ class Password
         // no extra iterations
         return NO_ITERATIONS;
     }
-
 }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Plugin.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Plugin.java
index 4f6ac92561..568ad1d78c 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Plugin.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/Plugin.java
@@ -18,14 +18,10 @@
  */
 package org.apache.felix.webconsole.internal.servlet;
 
-import java.net.URL;
-import java.util.Collections;
 import java.util.Enumeration;
 import java.util.NoSuchElementException;
 
-import org.apache.felix.http.javaxwrappers.ServletWrapper;
 import org.apache.felix.webconsole.internal.Util;
-import org.apache.felix.webconsole.internal.WebConsolePluginAdapter;
 import org.apache.felix.webconsole.servlet.AbstractServlet;
 import org.apache.felix.webconsole.servlet.ServletConstants;
 import org.osgi.framework.Bundle;
@@ -45,9 +41,9 @@ public class Plugin implements ServletConfig, 
Comparable<Plugin> {
 
     private final ServiceReference<Servlet> serviceReference; // used for 
comparing conflicting services
 
-    private final String title;
+    protected volatile String title;
 
-    private final String category;
+    protected volatile String category;
 
     private volatile Servlet consolePlugin;
 
@@ -160,28 +156,14 @@ public class Plugin implements ServletConfig, 
Comparable<Plugin> {
         return this.getServiceReference().toString();
     }
 
-    protected Servlet getService() {
-        return getHolder().getBundleContext().getService( 
this.getServiceReference() );
-    }
-
     protected Servlet doGetConsolePlugin() {
-        final Servlet service = getService();
+        final Servlet service = getHolder().getBundleContext().getService( 
this.getServiceReference() );
         if ( service != null ) {
+            final String[] css = Util.toStringArray( 
this.getServiceReference().getProperty( ServletConstants.PLUGIN_CSS_REFERENCES 
) );
             if ( service instanceof AbstractServlet ) {
-                return new JakartaServletAdapter((AbstractServlet)service, 
this.getServiceReference());
+                return new EnhancedPluginAdapter((AbstractServlet)service, 
this.getServiceReference(), this.getLabel(), this.getTitle(), css);
             }
-            final String prefix = "/".concat(this.getLabel());
-            final String resStart = prefix.concat("/res/");
-            return new ServletWrapper(service) {
-
-                @SuppressWarnings("unused")
-                public URL getResource(String path) {
-                    if (path != null && path.startsWith(resStart)) {
-                        return 
service.getClass().getResource(path.substring(prefix.length()));
-                    }
-                    return null;
-                }
-            };
+            return new SimplePluginAdapter(service, serviceReference, 
this.getLabel(), this.getTitle(), css);
         }
         return null;
     }
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java
index 53a2d2ab6e..f6779bc543 100644
--- 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/PluginHolder.java
@@ -135,10 +135,10 @@ public class PluginHolder implements 
ServiceTrackerCustomizer<Servlet, Plugin> {
         this.servletTracker.open();
         try {
             this.legacyTracker = new LegacyServicesTracker(this, 
this.getBundleContext());
-            this.osgiManager.log(LogService.LOG_INFO, "Servlet 2/3 bridge 
enabled");
+            this.osgiManager.log(LogService.LOG_INFO, "Servlet 3 bridge 
enabled");
         } catch ( final Throwable t) {
             // ignore
-            this.osgiManager.log(LogService.LOG_INFO, "Servlet 2/3 bridge not 
enabled");
+            this.osgiManager.log(LogService.LOG_INFO, "Servlet 3 bridge not 
enabled");
         }
     }
 
@@ -167,6 +167,10 @@ public class PluginHolder implements 
ServiceTrackerCustomizer<Servlet, Plugin> {
         this.defaultPluginLabel = null;
     }
 
+    public OsgiManager getOsgiManager() {
+        return this.osgiManager;
+    }
+
     /**
      * Returns label of the default plugin
      * @return label of the default plugin
@@ -307,7 +311,7 @@ public class PluginHolder implements 
ServiceTrackerCustomizer<Servlet, Plugin> {
      * Returns the bundle context of the Web Console itself.
      * @return the bundle context of the Web Console itself.
      */
-    BundleContext getBundleContext() {
+    public BundleContext getBundleContext() {
         return bundleContext;
     }
 
@@ -366,7 +370,7 @@ public class PluginHolder implements 
ServiceTrackerCustomizer<Servlet, Plugin> {
         removePlugin( plugin );
     }
 
-    void addPlugin( final Plugin plugin ) {
+    public void addPlugin( final Plugin plugin ) {
         synchronized ( plugins ) {
             final List<Plugin> list = 
plugins.computeIfAbsent(plugin.getLabel(), k -> new ArrayList<>());
             final Plugin oldPlugin = list.isEmpty() ? null : list.get(0);
@@ -391,7 +395,7 @@ public class PluginHolder implements 
ServiceTrackerCustomizer<Servlet, Plugin> {
         }
     }
 
-    void removePlugin( final Plugin plugin ) {
+    public void removePlugin( final Plugin plugin ) {
         synchronized ( plugins ) {
             final List<Plugin> list = plugins.get( plugin.getLabel() );
             if ( list != null ) {
diff --git 
a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/SimplePluginAdapter.java
 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/SimplePluginAdapter.java
new file mode 100644
index 0000000000..31cffaffaa
--- /dev/null
+++ 
b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/SimplePluginAdapter.java
@@ -0,0 +1,159 @@
+/*
+ * 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.felix.webconsole.internal.servlet;
+
+import java.io.IOException;
+import java.net.URL;
+import org.osgi.framework.ServiceReference;
+
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+
+/**
+ * The <code>SimplePluginAdapter</code> is an adapter to the
+ * {@link AbstractPluginAdapter} for regular servlets.
+ */
+public class SimplePluginAdapter extends AbstractPluginAdapter {
+
+    /** serial UID */
+    private static final long serialVersionUID = 1L;
+
+    // the actual plugin to forward rendering requests to
+    private final Servlet plugin;
+
+    private final String resStart;
+
+    /**
+     * Creates a new wrapper for a Web Console Plugin
+     */
+    public SimplePluginAdapter(final Servlet plugin, 
+            final ServiceReference<Servlet> serviceReference, 
+            final String label, final String title, final String[] 
cssReferences) {
+        super(serviceReference.getBundle().getBundleContext(), label, title, 
cssReferences);
+        this.plugin = plugin;
+        final String prefix = "/".concat(label);
+        this.resStart = prefix.concat("/res/");
+
+        // activate this abstract plugin (mainly to set the bundle context)
+        activate( serviceReference.getBundle().getBundleContext() );
+    }
+
+    /**
+     * Call the plugin servlet's service method to render the content of this
+     * page.
+     */
+    @Override
+    protected void renderContent(final HttpServletRequest req, final 
HttpServletResponse res )
+    throws ServletException, IOException {
+        plugin.service( req, res );
+    }
+
+    @Override
+    protected URL getResource(final String path) {
+        if (path != null && path.startsWith(resStart)) {
+            return 
this.plugin.getClass().getResource(path.substring(this.label.length() + 1));
+        }
+        return null;
+    }
+
+    //---------- Servlet API overwrite
+
+    /**
+     * Initializes this servlet as well as the plugin servlet.
+     */
+    @Override
+    public void init(final ServletConfig config ) throws ServletException {
+        // no need to activate the plugin, this has already been done
+        // when the instance was setup
+        try {
+            // base classe initialization
+            super.init( config );
+
+            // plugin initialization
+            plugin.init( config );
+        } catch (final ServletException se ) {
+            // if init fails, the plugin will not be destroyed and thus
+            // the plugin not deactivated. Do it here
+            deactivate();
+
+            // rethrow the exception
+            throw se;
+        }
+    }
+
+    /**
+     * Detects whether this request is intended to have the headers and
+     * footers of this plugin be rendered or not. The decision is taken based
+     * on whether and what extension the request URI has: If the request URI
+     * has no extension or the the extension is <code>.html</code>, the request
+     * is assumed to be rendered with header and footer. Otherwise the
+     * headers and footers are omitted and the
+     * {@link #renderContent(HttpServletRequest, HttpServletResponse)}
+     * method is called without any decorations and without setting any
+     * response headers.
+     */
+    @Override
+    protected boolean isHtmlRequest( final HttpServletRequest request ) {
+        final String requestUri = request.getRequestURI();
+        if ( requestUri.endsWith( ".html" ) ) {
+            return true;
+        }
+        // check if there is an extension
+        final int lastSlash = requestUri.lastIndexOf('/');
+        final int lastDot = requestUri.indexOf('.', lastSlash + 1);
+        return lastDot < 0;
+    }
+
+    /**
+     * Directly refer to the plugin's service method unless the request method
+     * is <code>GET</code> in which case we defer the call into the service 
method
+     * until the abstract web console plugin calls the
+     * {@link #renderContent(HttpServletRequest, HttpServletResponse)}
+     * method.
+     */
+    @Override
+    public void service( final ServletRequest req, final ServletResponse resp 
) throws ServletException, IOException {
+        if ( ( req instanceof HttpServletRequest ) && ( ( HttpServletRequest ) 
req ).getMethod().equals( "GET" ) ) {
+            // handle the GET request here and call into plugin on 
renderContent
+            super.service( req, resp );
+        } else {
+            // not a GET request, have the plugin handle it directly
+            plugin.service( req, resp );
+        }
+    }
+
+    /**
+     * Destroys this servlet as well as the plugin servlet.
+     */
+    @Override
+    public void destroy() {
+        try {
+            plugin.destroy();
+            super.destroy();
+        } finally {
+            deactivate();
+        }
+    }
+}
diff --git 
a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProviderTest.java
 
b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProviderTest.java
new file mode 100644
index 0000000000..65bcadde7a
--- /dev/null
+++ 
b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/BasicWebConsoleSecurityProviderTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.felix.webconsole.internal.servlet;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class BasicWebConsoleSecurityProviderTest {
+
+    @Test
+    public void testAuthenticate() throws Exception {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+
+        final BasicWebConsoleSecurityProvider provider = new 
BasicWebConsoleSecurityProvider(bc, "foo", "bar");
+        assertNotNull(provider.authenticate("foo", "bar"));
+        assertNull(provider.authenticate("foo", "blah"));
+    }
+
+    @Test
+    public void testAuthenticatePwdDisabledWithRequiredSecurityProvider() 
throws Exception {
+        final BundleContext bc = Mockito.mock(BundleContext.class);
+        
Mockito.when(bc.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS)).thenReturn("a");
+
+        final BasicWebConsoleSecurityProvider provider = new 
BasicWebConsoleSecurityProvider(bc, "foo", "bar");
+        assertNull(provider.authenticate("foo", "bar"));
+        assertNull(provider.authenticate("foo", "blah"));
+    }
+}
diff --git 
a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
 
b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
deleted file mode 100644
index c8b17a9c7c..0000000000
--- 
a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerHttpContextTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.felix.webconsole.internal.servlet;
-
-import org.apache.felix.webconsole.spi.SecurityProvider;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-
-import java.lang.reflect.Method;
-
-import static org.junit.Assert.assertEquals;
-
-public class OsgiManagerHttpContextTest {
-    @Test
-    public void testAuthenticate() throws Exception {
-        BundleContext bc = Mockito.mock(BundleContext.class);
-        Bundle bundle = Mockito.mock(Bundle.class);
-        OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(bundle, null);
-
-        Method authenticateMethod = 
OsgiManagerHttpContext.class.getDeclaredMethod(
-                "authenticate", new Class [] {SecurityProvider.class, 
String.class, byte[].class});
-        authenticateMethod.setAccessible(true);
-
-        BasicWebConsoleSecurityProvider lastResortSp = new 
BasicWebConsoleSecurityProvider(bc, "foo", "bar", "blah");
-        assertEquals(true, authenticateMethod.invoke(ctx, lastResortSp, "foo", 
"bar".getBytes()));
-        assertEquals(false, authenticateMethod.invoke(ctx, lastResortSp, 
"foo", "blah".getBytes()));
-
-        SecurityProvider sp = new TestSecurityProvider();
-        assertEquals(true, authenticateMethod.invoke(ctx, sp, "xxx", 
"yyy".getBytes()));
-        assertEquals("The default username and password should not be accepted 
with security provider",
-                false, authenticateMethod.invoke(ctx, sp, "foo", 
"bar".getBytes()));
-    }
-
-    @Test
-    public void testAuthenticatePwdDisabledWithRequiredSecurityProvider() 
throws Exception {
-        BundleContext bc = Mockito.mock(BundleContext.class);
-        
Mockito.when(bc.getProperty(OsgiManager.FRAMEWORK_PROP_SECURITY_PROVIDERS)).thenReturn("a");
-
-        Bundle bundle = Mockito.mock(Bundle.class);
-        OsgiManagerHttpContext ctx = new OsgiManagerHttpContext(bundle, null);
-
-        Method authenticateMethod = 
OsgiManagerHttpContext.class.getDeclaredMethod(
-                "authenticate", new Class [] {SecurityProvider.class, 
String.class, byte[].class});
-        authenticateMethod.setAccessible(true);
-
-        assertEquals("A required security provider is configured, logging in 
using "
-                + "username and password should be disabled",
-                false, authenticateMethod.invoke(ctx, null, "foo", 
"bar".getBytes()));
-        assertEquals(false, authenticateMethod.invoke(ctx, null, "foo", 
"blah".getBytes()));
-        assertEquals(false, authenticateMethod.invoke(ctx, null, "blah", 
"bar".getBytes()));
-
-        SecurityProvider sp = new TestSecurityProvider();
-        assertEquals(true, authenticateMethod.invoke(ctx, sp, "xxx", 
"yyy".getBytes()));
-        assertEquals(false, authenticateMethod.invoke(ctx, sp, "foo", 
"bar".getBytes()));
-    }
-
-    private static class TestSecurityProvider implements SecurityProvider {
-        @Override
-        public Object authenticate(String username, String password) {
-            if ("xxx".equals(username) && "yyy".equals(password))
-                return new Object();
-            return null;
-        }
-
-        @Override
-        public boolean authorize(Object user, String role) {
-            return false;
-        }
-    }
-}
diff --git 
a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java
 
b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java
index 9eea9e8102..76551650dd 100644
--- 
a/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java
+++ 
b/webconsole/src/test/java/org/apache/felix/webconsole/internal/servlet/OsgiManagerTest.java
@@ -35,8 +35,6 @@ import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
-import javax.servlet.Servlet;
-
 import org.apache.felix.webconsole.spi.SecurityProvider;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -53,6 +51,8 @@ import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.servlet.context.ServletContextHelper;
 import org.osgi.util.tracker.ServiceTrackerCustomizer;
 
+import jakarta.servlet.Servlet;
+
 public class OsgiManagerTest {
     @Test
     public void testSplitCommaSeparatedString() {
@@ -248,7 +248,7 @@ public class OsgiManagerTest {
             .registerService(Mockito.eq(SecurityProvider.class), 
Mockito.isA(SecurityProvider.class), Mockito.isA(Dictionary.class));
         Mockito.verify(bc, Mockito.times(1))
             .registerService(Mockito.eq(ServletContextHelper.class), 
Mockito.isA(ServletContextHelper.class), Mockito.isA(Dictionary.class));
-        Mockito.verify(bc, Mockito.times(1))
+        Mockito.verify(bc, Mockito.times(7))
             .registerService(Mockito.eq(Servlet.class), 
Mockito.isA(Servlet.class), Mockito.isA(Dictionary.class));
 
         mgr.registerHttpWhiteboardServices();
@@ -258,7 +258,7 @@ public class OsgiManagerTest {
             .registerService(Mockito.eq(SecurityProvider.class), 
Mockito.isA(SecurityProvider.class), Mockito.isA(Dictionary.class));
         Mockito.verify(bc, Mockito.times(1))
             .registerService(Mockito.eq(ServletContextHelper.class), 
Mockito.isA(ServletContextHelper.class), Mockito.isA(Dictionary.class));
-        Mockito.verify(bc, Mockito.times(1))
+        Mockito.verify(bc, Mockito.times(7))
             .registerService(Mockito.eq(Servlet.class), 
Mockito.isA(Servlet.class), Mockito.isA(Dictionary.class));
     }
 


Reply via email to