This is an automated email from the ASF dual-hosted git repository. cziegeler pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/felix-dev.git
commit 1dabc9ee5ba27a074b5a81f97ae7f2ef06802410 Author: Carsten Ziegeler <cziege...@apache.org> AuthorDate: Sat Sep 9 11:26:17 2023 +0200 FELIX-6638 : Migrate WebConsole to Jakarta Servlet API --- webconsole/pom.xml | 12 +- .../apache/felix/webconsole/WebConsoleUtil.java | 45 +- .../org/apache/felix/webconsole/internal/Util.java | 73 ++ .../internal/configuration/ConfigManager.java | 3 - .../webconsole/internal/core/BundlesServlet.java | 3 - .../webconsole/internal/core/ServicesServlet.java | 5 +- .../internal/filter/FilteringResponseWrapper.java | 2 - .../internal/legacy/LegacyServicesTracker.java | 79 +- .../{ => legacy}/WebConsolePluginAdapter.java | 57 +- .../internal/misc/ConfigurationRender.java | 11 +- .../internal/servlet/AbstractInternalPlugin.java | 984 --------------------- .../servlet/AbstractOsgiManagerPlugin.java | 3 - .../internal/servlet/AbstractPluginAdapter.java | 651 ++++++++++++++ .../servlet/BasicWebConsoleSecurityProvider.java | 4 +- .../internal/servlet/ConfigurationUtil.java | 1 + ...vletAdapter.java => EnhancedPluginAdapter.java} | 85 +- .../webconsole/internal/servlet/OsgiManager.java | 71 +- .../internal/servlet/OsgiManagerHttpContext.java | 1 + .../webconsole/internal/servlet/Password.java | 134 +-- .../felix/webconsole/internal/servlet/Plugin.java | 30 +- .../webconsole/internal/servlet/PluginHolder.java | 14 +- .../internal/servlet/SimplePluginAdapter.java | 159 ++++ .../BasicWebConsoleSecurityProviderTest.java | 48 + .../servlet/OsgiManagerHttpContextTest.java | 88 -- .../internal/servlet/OsgiManagerTest.java | 8 +- 25 files changed, 1108 insertions(+), 1463 deletions(-) diff --git a/webconsole/pom.xml b/webconsole/pom.xml index e44f6aafb4..0762efa603 100644 --- a/webconsole/pom.xml +++ b/webconsole/pom.xml @@ -118,10 +118,10 @@ </Export-Package> <!-- Import-Package header is also used for the all bundle --> <Import-Package> - javax.servlet.*;version="[3,5)", + jakarta.servlet.*;version="[5,7)", !javax.portlet, - !jakarta.servlet, - !jakarta.servlet.http, + !javax.servlet, + !javax.servlet.http, !org.apache.felix.http.javaxwrappers, !org.apache.felix.http.jakartawrappers, !org.apache.felix.bundlerepository, @@ -146,8 +146,8 @@ org.osgi.service.permissionadmin;version="[1.2,2)", org.osgi.service.prefs;version="[1.1,2)", org.osgi.service.wireadmin;version="[1.0,2)", - jakarta.servlet;version="[5,7)", - jakarta.servlet.http;version="[5,7)", + javax.servlet;version="[3.1,5)", + javax.servlet.http;version="[3.1,5)", org.apache.felix.http.javaxwrappers;version="[1,2)", org.apache.felix.http.jakartawrappers;version="[1,2)" </DynamicImport-Package> @@ -368,7 +368,7 @@ <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> - <version>${servlet.api}</version> + <version>${servlet.api}</version> <scope>provided</scope> </dependency> <dependency> diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java b/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java index 2f6c2c6552..7b95414f16 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/WebConsoleUtil.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +39,7 @@ import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.servlet.ServletRequestContext; +import org.apache.felix.webconsole.internal.Util; /** @@ -325,19 +327,13 @@ public final class WebConsoleUtil * @param value the value to decode * @return the decoded string */ - public static String urlDecode( final String value ) - { + public static String urlDecode( final String value ) { // shortcut for empty or missing values - if ( value == null || value.length() == 0 ) - { + if ( value == null || value.length() == 0 ) { return value; } - try { - return URLDecoder.decode( value, "UTF-8" ); - } catch (UnsupportedEncodingException e) { - return URLDecoder.decode( value ); - } + return URLDecoder.decode(value, StandardCharsets.UTF_8); } /** @@ -351,35 +347,6 @@ public final class WebConsoleUtil * @return the string representation of the value */ public static final String toString(Object value) { - if (value == null) { - return "n/a"; - } else if (value.getClass().isArray()) { - final StringBuilder sb = new StringBuilder(); - int len = Array.getLength(value); - sb.append('['); - - for(int i = 0; i < len; ++i) { - final Object element = Array.get(value, i); - if (element instanceof Byte) { - sb.append("0x"); - final String x = Integer.toHexString(((Byte)element).intValue() & 255); - if (1 == x.length()) { - sb.append('0'); - } - - sb.append(x); - } else { - sb.append(toString(element)); - } - - if (i < len - 1) { - sb.append(", "); - } - } - - return sb.append(']').toString(); - } else { - return value.toString(); - } + return Util.toString(value); } } diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java index bc7bb81ba1..7ed3ea62ef 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/Util.java @@ -18,8 +18,11 @@ package org.apache.felix.webconsole.internal; import java.io.IOException; +import java.lang.reflect.Array; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; +import java.util.Iterator; import java.util.Locale; import org.osgi.framework.Bundle; @@ -260,4 +263,74 @@ public class Util { } response.sendRedirect(redirectUrl); } + + @SuppressWarnings("rawtypes") + public static String[] toStringArray( final Object value ) { + if ( value instanceof String ) { + return new String[] { ( String ) value }; + } else if ( value != null ) { + final Collection col; + if ( value.getClass().isArray() ) { + col = Arrays.asList( ( Object[] ) value ); + } else if ( value instanceof Collection ) { + col = ( Collection ) value; + } else { + col = null; + } + + if ( col != null && !col.isEmpty() ) { + final String[] entries = new String[col.size()]; + int i = 0; + for (final Iterator cli = col.iterator(); cli.hasNext(); i++ ) { + entries[i] = String.valueOf( cli.next() ); + } + return entries; + } + } + + return null; + } + + /** + * This method will stringify a Java object. It is mostly used to print the values + * of unknown properties. This method will correctly handle if the passed object + * is array and will property display it. + * + * If the value is byte[] the elements are shown as Hex + * + * @param value the value to convert + * @return the string representation of the value + */ + public static final String toString(Object value) { + if (value == null) { + return "n/a"; + } else if (value.getClass().isArray()) { + final StringBuilder sb = new StringBuilder(); + int len = Array.getLength(value); + sb.append('['); + + for(int i = 0; i < len; ++i) { + final Object element = Array.get(value, i); + if (element instanceof Byte) { + sb.append("0x"); + final String x = Integer.toHexString(((Byte)element).intValue() & 255); + if (1 == x.length()) { + sb.append('0'); + } + + sb.append(x); + } else { + sb.append(toString(element)); + } + + if (i < len - 1) { + sb.append(", "); + } + } + + return sb.append(']').toString(); + } else { + return value.toString(); + } + } } \ No newline at end of file diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java index 2d52a5d57c..38506ab3fc 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/configuration/ConfigManager.java @@ -361,9 +361,6 @@ public class ConfigManager extends AbstractOsgiManagerPlugin { super.doGet( request, response ); } - /** - * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ @Override public void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException { diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java index f330899e4a..a684347baf 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java @@ -507,9 +507,6 @@ public class BundlesServlet extends AbstractOsgiManagerPlugin implements Invento buf.append(msg); } - /** - * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ @Override public void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException { diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java index 6abd54efd5..48c00e41fd 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/ServicesServlet.java @@ -451,10 +451,7 @@ public class ServicesServlet extends AbstractOsgiManagerPlugin super.doGet( request, response ); } - - /** - * @see org.apache.felix.webconsole.AbstractWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ + @Override public void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException { // get request info from request attribute final RequestInfo reqInfo = getRequestInfo( request ); diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java index 76f2b1dd2b..1be153c17c 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/filter/FilteringResponseWrapper.java @@ -73,8 +73,6 @@ public class FilteringResponseWrapper extends HttpServletResponseWrapper { * is being generated a filtering writer is returned which translates * strings enclosed in <code>${}</code> according to the resource bundle * configured for this response. - * - * @see javax.servlet.ServletResponseWrapper#getWriter() */ public PrintWriter getWriter() throws IOException { if ( writer == null ) { diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java index 1a6798be31..e8850ab089 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/LegacyServicesTracker.java @@ -20,25 +20,22 @@ package org.apache.felix.webconsole.internal.legacy; import java.io.Closeable; -import java.net.URL; import java.util.Dictionary; import java.util.Hashtable; import javax.servlet.Servlet; +import org.apache.felix.http.jakartawrappers.ServletWrapper; import org.apache.felix.http.javaxwrappers.HttpServletRequestWrapper; import org.apache.felix.http.javaxwrappers.HttpServletResponseWrapper; -import org.apache.felix.http.javaxwrappers.ServletWrapper; -import org.apache.felix.webconsole.WebConsoleConstants; +import org.apache.felix.webconsole.AbstractWebConsolePlugin; import org.apache.felix.webconsole.WebConsoleSecurityProvider; import org.apache.felix.webconsole.WebConsoleSecurityProvider2; import org.apache.felix.webconsole.WebConsoleSecurityProvider3; import org.apache.felix.webconsole.internal.Util; import org.apache.felix.webconsole.internal.servlet.BasicWebConsoleSecurityProvider; -import org.apache.felix.webconsole.internal.servlet.OsgiManager; import org.apache.felix.webconsole.internal.servlet.Plugin; import org.apache.felix.webconsole.internal.servlet.PluginHolder; -import org.apache.felix.webconsole.servlet.AbstractServlet; import org.apache.felix.webconsole.servlet.ServletConstants; import org.apache.felix.webconsole.spi.SecurityProvider; import org.osgi.framework.BundleContext; @@ -47,16 +44,17 @@ import org.osgi.framework.Filter; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; import org.osgi.util.tracker.ServiceTracker; import org.osgi.util.tracker.ServiceTrackerCustomizer; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +@SuppressWarnings("deprecation") +public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomizer<Servlet, LegacyServicesTracker.LegacyServletPlugin> { -public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomizer<Servlet, LegacyServicesTracker.JakartaServletPlugin> { - - private final ServiceTracker<Servlet, JakartaServletPlugin> servletTracker; + private final ServiceTracker<Servlet, LegacyServletPlugin> servletTracker; private final PluginHolder pluginHolder; @@ -76,7 +74,7 @@ public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomize } this.servletTracker = new ServiceTracker<>(context, filter, this); servletTracker.open(); - this.securityProviderTracker = new LegacySecurityProviderTracker(context); + this.securityProviderTracker = new LegacySecurityProviderTracker(pluginHolder, context); } @Override @@ -86,10 +84,13 @@ public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomize } @Override - public JakartaServletPlugin addingService( final ServiceReference<Servlet> reference ) { - final String label = Util.getStringProperty( reference, WebConsoleConstants.PLUGIN_LABEL ); + public LegacyServletPlugin addingService( final ServiceReference<Servlet> reference ) { + final String label = Util.getStringProperty( reference, ServletConstants.PLUGIN_LABEL ); if ( label != null ) { - final JakartaServletPlugin plugin = new JakartaServletPlugin(this.pluginHolder, reference, label); + this.pluginHolder.getOsgiManager().log(LogService.LOG_WARNING, + "Legacy webconsole plugin found. Update this to the Jakarta Servlet API: " + reference); + + final LegacyServletPlugin plugin = new LegacyServletPlugin(this.pluginHolder, reference, label); pluginHolder.addPlugin(plugin); return plugin; } @@ -97,56 +98,55 @@ public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomize } @Override - public void modifiedService( final ServiceReference<Servlet> reference, final JakartaServletPlugin service ) { + public void modifiedService( final ServiceReference<Servlet> reference, final LegacyServletPlugin service ) { this.removedService(reference, service); this.addingService(reference); } @Override - public void removedService( final ServiceReference<Servlet> reference, final JakartaServletPlugin service ) { + public void removedService( final ServiceReference<Servlet> reference, final LegacyServletPlugin service ) { this.pluginHolder.removePlugin(service); } - public static class JakartaServletPlugin extends Plugin { + public static class LegacyServletPlugin extends Plugin { - @SuppressWarnings({"unchecked", "rawtypes"}) - public JakartaServletPlugin(PluginHolder holder, ServiceReference<jakarta.servlet.Servlet> serviceReference, - String label) { + @SuppressWarnings({ "rawtypes", "unchecked"}) + public LegacyServletPlugin(final PluginHolder holder, final ServiceReference<Servlet> serviceReference, final String label) { super(holder, (ServiceReference)serviceReference, label); } - protected jakarta.servlet.Servlet getService() { - final Servlet servlet = (Servlet) getHolder().getBundleContext().getService( (ServiceReference)this.getServiceReference() ); + @SuppressWarnings({ "rawtypes", "unchecked"}) + @Override + protected jakarta.servlet.Servlet doGetConsolePlugin() { + Servlet servlet = (Servlet) getHolder().getBundleContext().getService( (ServiceReference)this.getServiceReference() ); if (servlet != null) { - if ( servlet instanceof AbstractServlet ) { - return new JakartaServletAdapter((AbstractServlet)servlet, this.getServiceReference()); - } - final String prefix = "/".concat(this.getLabel()); - final String resStart = prefix.concat("/res/"); - return new ServletWrapper(servlet) { - - @SuppressWarnings("unused") - public URL getResource(String path) { - if (path != null && path.startsWith(resStart)) { - return servlet.getClass().getResource(path.substring(prefix.length())); - } - return null; + if ( servlet instanceof AbstractWebConsolePlugin ) { + if (this.title == null) { + this.title = ((AbstractWebConsolePlugin)servlet).getTitle(); + } + if (this.category == null) { + this.category = ((AbstractWebConsolePlugin)servlet).getCategory(); } - }; + } else { + servlet = new WebConsolePluginAdapter(getLabel(), servlet, (ServiceReference)this.getServiceReference()); + } + return new ServletWrapper(servlet); } return null; } } - @SuppressWarnings("deprecation") public static class LegacySecurityProviderTracker implements ServiceTrackerCustomizer<WebConsoleSecurityProvider, ServiceRegistration<SecurityProvider>> { private final ServiceTracker<WebConsoleSecurityProvider, ServiceRegistration<SecurityProvider>> tracker; private final BundleContext bundleContext; - public LegacySecurityProviderTracker( final BundleContext context ) { + private final PluginHolder pluginHolder; + + public LegacySecurityProviderTracker(final PluginHolder holder, final BundleContext context ) { this.bundleContext = context; + this.pluginHolder = holder; this.tracker = new ServiceTracker<>(context, WebConsoleSecurityProvider.class, this); tracker.open(); } @@ -157,6 +157,8 @@ public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomize @Override public ServiceRegistration<SecurityProvider> addingService( final ServiceReference<WebConsoleSecurityProvider> reference ) { + this.pluginHolder.getOsgiManager().log(LogService.LOG_WARNING, + "Legacy webconsole plugin found. Update this to the Jakarta Servlet API: " + reference); final WebConsoleSecurityProvider provider = this.bundleContext.getService(reference); if ( provider != null ) { final SecurityProvider wrapper = provider instanceof WebConsoleSecurityProvider2 @@ -178,8 +180,8 @@ public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomize if (reference.getProperty(Constants.SERVICE_RANKING) != null) { props.put(Constants.SERVICE_RANKING, reference.getProperty(Constants.SERVICE_RANKING)); } - if (reference.getProperty(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME) != null) { - props.put(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME, reference.getProperty(OsgiManager.SECURITY_PROVIDER_PROPERTY_NAME)); + if (reference.getProperty(SecurityProvider.PROPERTY_ID) != null) { + props.put(SecurityProvider.PROPERTY_ID, reference.getProperty(SecurityProvider.PROPERTY_ID)); } return reference.getBundle().getBundleContext().registerService(SecurityProvider.class, wrapper, props); } @@ -202,7 +204,6 @@ public class LegacyServicesTracker implements Closeable, ServiceTrackerCustomize } } - @SuppressWarnings("deprecation") public static class SecurityProviderWrapper implements SecurityProvider { private final WebConsoleSecurityProvider provider; diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/WebConsolePluginAdapter.java similarity index 84% rename from webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java rename to webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/WebConsolePluginAdapter.java index 37aa34687d..6386ea176d 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/WebConsolePluginAdapter.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/legacy/WebConsolePluginAdapter.java @@ -16,18 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.felix.webconsole.internal; +package org.apache.felix.webconsole.internal.legacy; import java.io.IOException; -import java.util.*; - -import javax.servlet.*; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.felix.webconsole.AbstractWebConsolePlugin; import org.apache.felix.webconsole.WebConsoleConstants; +import org.apache.felix.webconsole.internal.Util; +import org.apache.felix.webconsole.servlet.ServletConstants; import org.osgi.framework.ServiceReference; @@ -37,6 +41,7 @@ import org.osgi.framework.ServiceReference; * {@link org.apache.felix.webconsole.WebConsoleConstants#PLUGIN_TITLE} * service attribute. */ +@SuppressWarnings("deprecation") public class WebConsolePluginAdapter extends AbstractWebConsolePlugin { @@ -63,7 +68,7 @@ public class WebConsolePluginAdapter extends AbstractWebConsolePlugin { this.label = label; this.plugin = plugin; - this.cssReferences = toStringArray( serviceReference.getProperty( WebConsoleConstants.PLUGIN_CSS_REFERENCES ) ); + this.cssReferences = Util.toStringArray( serviceReference.getProperty( ServletConstants.PLUGIN_CSS_REFERENCES ) ); // activate this abstract plugin (mainly to set the bundle context) activate( serviceReference.getBundle().getBundleContext() ); @@ -235,46 +240,4 @@ public class WebConsolePluginAdapter extends AbstractWebConsolePlugin deactivate(); } } - - - //---------- internal - - @SuppressWarnings("rawtypes") - private String[] toStringArray( final Object value ) - { - if ( value instanceof String ) - { - return new String[] - { ( String ) value }; - } - else if ( value != null ) - { - final Collection cssListColl; - if ( value.getClass().isArray() ) - { - cssListColl = Arrays.asList( ( Object[] ) value ); - } - else if ( value instanceof Collection ) - { - cssListColl = ( Collection ) value; - } - else - { - cssListColl = null; - } - - if ( cssListColl != null && !cssListColl.isEmpty() ) - { - String[] entries = new String[cssListColl.size()]; - int i = 0; - for ( Iterator cli = cssListColl.iterator(); cli.hasNext(); i++ ) - { - entries[i] = String.valueOf( cli.next() ); - } - return entries; - } - } - - return null; - } } diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java index ac3aed49ed..1edd8f3c02 100644 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java +++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/misc/ConfigurationRender.java @@ -19,11 +19,9 @@ package org.apache.felix.webconsole.internal.misc; import java.io.PrintWriter; -import org.apache.felix.webconsole.WebConsoleUtil; +import org.apache.felix.webconsole.internal.Util; - -public class ConfigurationRender -{ +public class ConfigurationRender { /** * Renders an info line - element in the framework configuration. The info line will @@ -41,8 +39,7 @@ public class ConfigurationRender */ public static final void infoLine( PrintWriter pw, String indent, String label, Object value ) { - if ( indent != null ) - { + if ( indent != null ) { pw.print( indent ); } @@ -52,7 +49,7 @@ public class ConfigurationRender pw.print( " = " ); } - pw.print( WebConsoleUtil.toString( value ) ); + pw.print( Util.toString( value ) ); pw.println(); } diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractInternalPlugin.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractInternalPlugin.java deleted file mode 100644 index b2198e6084..0000000000 --- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/AbstractInternalPlugin.java +++ /dev/null @@ -1,984 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.felix.webconsole.internal.servlet; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.URL; -import java.net.URLConnection; -import java.nio.charset.StandardCharsets; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.Dictionary; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.ResourceBundle; -import java.util.SortedMap; -import java.util.TreeMap; - -import org.apache.felix.webconsole.DefaultBrandingPlugin; -import org.apache.felix.webconsole.SimpleWebConsolePlugin; -import org.apache.felix.webconsole.WebConsoleConstants; -import org.apache.felix.webconsole.WebConsoleUtil; -import org.apache.felix.webconsole.i18n.LocalizationHelper; -import org.apache.felix.webconsole.internal.servlet.OsgiManager; -import org.apache.felix.webconsole.servlet.RequestVariableResolver; -import org.apache.felix.webconsole.servlet.ServletConstants; -import org.apache.felix.webconsole.spi.BrandingPlugin; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.log.LogService; -import org.osgi.util.tracker.ServiceTracker; -import org.osgi.util.tracker.ServiceTrackerCustomizer; - -import jakarta.servlet.Servlet; -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * This is an internal base class for web console plugins - */ -public abstract class AbstractInternalPlugin extends HttpServlet { - - /** - * The name of the request attribute providing a mapping of labels to page - * titles of registered console plugins (value is "felix.webconsole.labelMap"). - * This map may be used to render a navigation of the console plugins as the - * {@link AbstractWebConsolePlugin#renderTopNavigation(javax.servlet.http.HttpServletRequest, java.io.PrintWriter)} - * method does. - * <p> - * The type of this request attribute is <code>Map<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)); }