You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by cz...@apache.org on 2023/09/09 12:46:33 UTC

[felix-dev] branch master updated: FELIX-6652 : Remove deprecated rendering attributes

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


The following commit(s) were added to refs/heads/master by this push:
     new 39668e8fa9 FELIX-6652 : Remove deprecated rendering attributes
39668e8fa9 is described below

commit 39668e8fa9d475c46e39e0be2be5976aec1bc553
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Sat Sep 9 14:46:24 2023 +0200

    FELIX-6652 : Remove deprecated rendering attributes
---
 .../felix/webconsole/AbstractWebConsolePlugin.java | 300 ++-------------------
 .../webconsole/internal/NavigationRenderer.java    | 201 ++++++++++++++
 .../org/apache/felix/webconsole/internal/Util.java |  28 ++
 .../servlet/AbstractOsgiManagerPlugin.java         |   6 +
 .../internal/servlet/AbstractPluginAdapter.java    | 215 +--------------
 .../webconsole/internal/servlet/OsgiManager.java   |  34 +--
 .../felix/webconsole/servlet/AbstractServlet.java  |  30 +--
 7 files changed, 264 insertions(+), 550 deletions(-)

diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java b/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
index e66125b517..b16001433e 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/AbstractWebConsolePlugin.java
@@ -21,30 +21,24 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.PrintWriter;
-import java.io.StringWriter;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 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.Iterator;
-import java.util.Locale;
 import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.felix.webconsole.internal.NavigationRenderer;
 import org.apache.felix.webconsole.internal.Util;
-import org.apache.felix.webconsole.internal.servlet.OsgiManager;
+import org.apache.felix.webconsole.internal.servlet.AbstractOsgiManagerPlugin;
 import org.apache.felix.webconsole.servlet.RequestVariableResolver;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -108,16 +102,6 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
      */
     public static final String GET_RESOURCE_METHOD_NAME = "getResource";
 
-    /**
-     * 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;
-
     /**
      * The reference to the getResource method provided by the
      * {@link #getResourceProvider()}. This is <code>null</code> if there is
@@ -617,7 +601,7 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
         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() );
+        pw.println( NavigationRenderer.HEADER );
 
         return pw;
     }
@@ -630,118 +614,17 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
      * @param pw the writer, where the HTML data is rendered
      */
     @SuppressWarnings({ "rawtypes" })
-    protected 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( WebConsoleConstants.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>" );
-        }
-    }
-
+    protected void renderTopNavigation(final HttpServletRequest request, final PrintWriter pw ) {
+        final String appRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_APP_ROOT );
+        final Map menuMap = ( Map ) request.getAttribute( AbstractOsgiManagerPlugin.ATTR_LABEL_MAP_CATEGORIZED );
+        final Map langMap = (Map) request.getAttribute(WebConsoleConstants.ATTR_LANG_MAP);
 
-    @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>" );
+        NavigationRenderer.renderTopNavigation(pw, appRoot, menuMap, langMap, request.getLocale());
     }
 
-
     @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("'/>");
+    protected void renderMenu(final Map menuMap, final String appRoot, final PrintWriter pw ) {
+        throw new UnsupportedOperationException();
     }
 
     /**
@@ -750,12 +633,10 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
      * @param pw the writer, where the HTML data is rendered
      * @see #startResponse(HttpServletRequest, HttpServletResponse)
      */
-    protected void endResponse( PrintWriter pw )
-    {
-        pw.println(getFooter());
+    protected void endResponse( PrintWriter pw ) {
+        pw.println(NavigationRenderer.FOOTER);
     }
 
-
     /**
      * An utility method, that is used to filter out simple parameter from file
      * parameter when multipart transfer encoding is used.
@@ -843,36 +724,6 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
         // nothing to do
     }
 
-    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( AbstractWebConsolePlugin.class, "/templates/main_header.html" );
-        }
-        return HEADER;
-    }
-
-
-    private final String getFooter()
-    {
-        if ( FOOTER == null )
-        {
-            FOOTER = readTemplateFile( AbstractWebConsolePlugin.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
@@ -892,43 +743,14 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
      *      exception thrown as its cause.
      */
     protected final String readTemplateFile( final String templateFile ) {
-        return readTemplateFile( getClass(), templateFile );
-    }
-
-    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 );
+        try {
+            return Util.readTemplateFile( getClass(), templateFile );
+        } catch (final IOException e) {
+            Util.LOGGER.error("readTemplateFile: File '{}' not found through class {}", templateFile, getClass() );
+            return "";
         }
-
-        // 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 )
     {
         // get the CSS references and return nothing if there are none
@@ -972,50 +794,7 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
         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.
      * <p>
      * The resolver is added to the request attributes via the web console main
@@ -1035,47 +814,4 @@ public abstract class AbstractWebConsolePlugin extends HttpServlet {
     public 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/NavigationRenderer.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/NavigationRenderer.java
new file mode 100644
index 0000000000..39d6bf4739
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/NavigationRenderer.java
@@ -0,0 +1,201 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+public class NavigationRenderer {
+
+    /**
+     * The header fragment read from the templates/main_header.html file
+     */
+    public static final String HEADER;
+
+    /**
+     * The footer fragment read from the templates/main_footer.html file
+     */
+    public static final String FOOTER;
+
+    static {
+        try {
+            HEADER = Util.readTemplateFile( NavigationRenderer.class, "/templates/main_header.html" );
+            FOOTER = Util.readTemplateFile(NavigationRenderer.class, "/templates/main_footer.html" );
+        } catch ( final IOException ioe) {
+            throw new RuntimeException(ioe);
+        }
+    }
+
+    /**
+     * 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" })
+    public static void renderTopNavigation( final PrintWriter pw, final String appRoot, final Map menuMap, final Map langMap, final Locale reqLocale) {
+        renderMenu( menuMap, appRoot, pw );
+
+        // render lang-box
+        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
+            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({ "unchecked", "rawtypes" })
+    private static 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;
+    }
+
+    @SuppressWarnings({ "rawtypes" })
+    private static 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 static 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 static 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(final PrintWriter pw, final String appRoot, final Object langCode, final 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("'/>");
+    }
+
+    @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/Util.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java
index 4a7d9db1eb..8f990b4205 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
@@ -17,8 +17,12 @@
 package org.apache.felix.webconsole.internal;
 
 
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
 import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
@@ -341,6 +345,30 @@ public class Util {
             ref.getBundle().getSymbolicName() + ":" + ref.getBundle().getVersion() + "(" + ref.getBundle().getBundleId() + ")";
     }
 
+    public static final String readTemplateFile( final Class<?> clazz, final String templateFile) throws IOException {
+        try(final 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;
+                }
+            }
+        }
+
+        throw new FileNotFoundException("Template " + templateFile + " not found");
+    }
+
     /** Logger for the webconsole */
     public static final Logger LOGGER = LoggerFactory.getLogger("org.apache.felix.webconsole");    
 }
\ No newline at end of file
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 67b0983ba0..a3d46d68c0 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
@@ -44,6 +44,12 @@ public abstract class AbstractOsgiManagerPlugin extends AbstractServlet implemen
      */
     public static final String ATTR_LABEL_MAP = "felix.webconsole.labelMap";
 
+    /**
+     * The name of the (internal) request attribute providing the categorized
+     * label map structure.
+     */
+    public static final String ATTR_LABEL_MAP_CATEGORIZED = AbstractOsgiManagerPlugin.ATTR_LABEL_MAP + ".categorized";
+
     // used to obtain services. Structure is: service name -> ServiceTracker
     private final Map<String, ServiceTracker<?, ?>> services = new HashMap<>();
 
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
index 205921759c..2bf18ca20f 100644
--- 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
@@ -20,16 +20,10 @@ 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.internal.NavigationRenderer;
 import org.apache.felix.webconsole.servlet.RequestVariableResolver;
 import org.apache.felix.webconsole.servlet.ServletConstants;
 import org.apache.felix.webconsole.spi.BrandingPlugin;
@@ -51,16 +45,6 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
     /** 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 volatile BundleContext bundleContext;
@@ -130,7 +114,7 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
             // 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 );
+                final PrintWriter pw = startResponse( request, response );
 
                 // render top navigation
                 renderTopNavigation( request, pw );
@@ -284,7 +268,7 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
         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 );
+        pw.println( NavigationRenderer.HEADER );
 
         return pw;
     }
@@ -297,102 +281,11 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
      */
     @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 );
+        final Map menuMap = ( Map ) request.getAttribute( AbstractOsgiManagerPlugin.ATTR_LABEL_MAP_CATEGORIZED );
+        final Map langMap = (Map) request.getAttribute(ATTR_LANG_MAP);
 
-        @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("'/>");
+        NavigationRenderer.renderTopNavigation(pw, appRoot, menuMap, langMap, request.getLocale());
     }
 
     /**
@@ -402,7 +295,7 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
      * @see #startResponse(HttpServletRequest, HttpServletResponse)
      */
     private void endResponse( PrintWriter pw ) {
-        pw.println(FOOTER);
+        pw.println(NavigationRenderer.FOOTER);
     }
 
     /**
@@ -422,35 +315,6 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
         }
     }
 
-    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) {
@@ -487,36 +351,6 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
         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
@@ -525,39 +359,4 @@ public abstract class AbstractPluginAdapter extends HttpServlet {
     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/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index 845d4d0e52..966e588c44 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
@@ -81,34 +81,6 @@ public class OsgiManager extends HttpServlet {
     /** Pseudo class version ID to keep the IDE quite. */
     private static final long serialVersionUID = 1L;
 
-    /**
-     * Old name of the request attribute providing the root to the web console.
-     * This attribute is no deprecated and replaced by
-     * {@link ServletConstants#ATTR_APP_ROOT}.
-     *
-     * @deprecated use {@link ServletConstants#ATTR_APP_ROOT} instead
-     */
-    @Deprecated
-    private static final String ATTR_APP_ROOT_OLD = OsgiManager.class.getName() + ".appRoot";
-
-    /**
-     * Old name of the request attribute providing the mappings from label to
-     * page title. This attribute is now deprecated and replaced by
-     * {@link AbstractOsgiManagerPlugin#ATTR_LABEL_MAP}.
-     *
-     * @deprecated use {@link AbstractOsgiManagerPlugin#ATTR_LABEL_MAP} instead
-     */
-    @Deprecated
-    private static final String ATTR_LABEL_MAP_OLD = OsgiManager.class.getName() + ".labelMap";
-
-    /**
-     * The name of the (internal) request attribute providing the categorized
-     * label map structure.
-     * @deprecated use {@link AbstractOsgiManagerPlugin#ATTR_LABEL_MAP} instead
-     */
-    @Deprecated
-    public static final String ATTR_LABEL_MAP_CATEGORIZED = AbstractOsgiManagerPlugin.ATTR_LABEL_MAP + ".categorized";
-
     /**
      * The name and value of a parameter which will prevent redirection to a
      * render after the action has been executed (value is "_noredir_"). This
@@ -553,16 +525,12 @@ public class OsgiManager extends HttpServlet {
         // the official request attributes
         request.setAttribute(org.apache.felix.webconsole.WebConsoleConstants.ATTR_LANG_MAP, getLangMap());
         request.setAttribute(AbstractOsgiManagerPlugin.ATTR_LABEL_MAP, flatLabelMap);
-        request.setAttribute( ATTR_LABEL_MAP_CATEGORIZED, labelMap );
+        request.setAttribute(AbstractOsgiManagerPlugin.ATTR_LABEL_MAP_CATEGORIZED, labelMap );
         final String appRoot = request.getContextPath().concat(request.getServletPath());
         request.setAttribute(ServletConstants.ATTR_APP_ROOT, appRoot);
         request.setAttribute(ServletConstants.ATTR_PLUGIN_ROOT, appRoot.concat(postfix));
         request.setAttribute(ServletConstants.ATTR_CONFIGURATION, configuration);
 
-        // deprecated request attributes
-        request.setAttribute(ATTR_LABEL_MAP_OLD, flatLabelMap);
-        request.setAttribute(ATTR_APP_ROOT_OLD, appRoot);
-
         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 ) );
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/servlet/AbstractServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/servlet/AbstractServlet.java
index 71a848f359..da36a50841 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/servlet/AbstractServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/servlet/AbstractServlet.java
@@ -23,10 +23,10 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.StringWriter;
 import java.net.URL;
 import java.net.URLConnection;
-import java.nio.charset.StandardCharsets;
+
+import org.apache.felix.webconsole.internal.Util;
 
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServlet;
@@ -192,31 +192,7 @@ public abstract class AbstractServlet extends HttpServlet {
      * @throws IOException On any other error reading the template file
      */
     protected final String readTemplateFile( final String templateFile ) throws IOException {
-        return readTemplateFile( getClass(), templateFile );
-    }
-
-    private final String readTemplateFile( final Class<?> clazz, final String templateFile) throws IOException {
-        try(final 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;
-                }
-            }
-        }
-
-        throw new FileNotFoundException("Template " + templateFile + " not found");
+        return Util.readTemplateFile( getClass(), templateFile );
     }
 
     protected RequestVariableResolver getVariableResolver(final HttpServletRequest request) {