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 09:26:30 UTC
[felix-dev] 04/04: FELIX-6638 : Migrate WebConsole to Jakarta Servlet API
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 <cz...@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<String, String></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<String, String></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));
}