All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.felix.webconsole.AbstractWebConsolePlugin Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
/*
 * 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;


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.felix.webconsole.internal.NavigationRenderer;
import org.apache.felix.webconsole.internal.Util;
import org.apache.felix.webconsole.internal.servlet.AbstractOsgiManagerPlugin;
import org.apache.felix.webconsole.servlet.RequestVariableResolver;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.log.LogService;


/**
 * The Web Console can be extended by registering an OSGi service for the interface
 * {@link javax.servlet.Servlet} with the service property
 * felix.webconsole.label set to the label (last segment in the URL)
 * of the page. The respective service is called a Web Console Plugin or a plugin
 * for short.
 *
 * To help rendering the response the Apache Felix Web Console bundle provides two
 * options. One of the options is to extend the AbstractWebConsolePlugin overwriting
 * the {@link #renderContent(HttpServletRequest, HttpServletResponse)} method.
 *
 * @deprecated Either register a servlet using Servlet API 5 or use {@link org.apache.felix.webconsole.servlet.AbstractServlet}
 */
@Deprecated
public abstract class AbstractWebConsolePlugin extends HttpServlet {

    /** Pseudo class version ID to keep the IDE quite. */
    private static final long serialVersionUID = 1L;

    /**
     * This attribute is not supported anymore
     * @deprecated Use the Servlet API for uploads
     */
    @Deprecated
    public static final String ATTR_FILEUPLOAD = "org.apache.felix.webconsole.fileupload";
    
    /**
     * This attribute is not supported anymore
     * @deprecated Use the Servlet API for uploads
     */
    @Deprecated
    public static final String ATTR_FILEUPLOAD_REPO = "org.apache.felix.webconsole.fileupload.repo";

    /**
     * Web Console Plugin typically consists of servlet and resources such as images,
     * scripts or style sheets.
     *
     * To load resources, a Resource Provider is used. The resource provider is an object,
     * that provides a method which name is specified by this constants and it is
     * 'getResource'.
     *
     *  @see #getResourceProvider()
     */
    public static final String GET_RESOURCE_METHOD_NAME = "getResource";

    /**
     * The reference to the getResource method provided by the
     * {@link #getResourceProvider()}. This is null if there is
     * none or before the first check if there is one.
     *
     * @see #getGetResourceMethod()
     */
    private Method getResourceMethod;

    /**
     * flag indicating whether the getResource method has already been looked
     * up or not. This prevens the {@link #getGetResourceMethod()} method from
     * repeatedly looking up the resource method on plugins which do not have
     * one.
     */
    private boolean getResourceMethodChecked;

    private BundleContext bundleContext;

    private static volatile BrandingPlugin BRANDING_PLUGIN = DefaultBrandingPlugin.getInstance();


    //---------- HttpServlet Overwrites ----------------------------------------

    /**
     * Returns the title for this plugin as returned by {@link #getTitle()}
     *
     * @see javax.servlet.GenericServlet#getServletName()
     */
    public String getServletName()
    {
        return getTitle();
    }


    /**
     * This method should return category string which will be used to render
     * the plugin in the navigation menu. Default implementation returns null,
     * which will result in the plugin link rendered as top level menu item.
     * Concrete implementations wishing to be rendered as a sub-menu item under
     * a category should override this method and return a string or define
     * felix.webconsole.category OSGi property. Currently only
     * single level categories are supported. So, this should be a simple
     * String.
     *
     * @return category
     */
    public String getCategory()
    {
        return null;
    }


    /**
     * Renders the web console page for the request. This consist of the
     * following five parts called in order:
     * 
    *
  1. Send back a requested resource *
  2. {@link #startResponse(HttpServletRequest, HttpServletResponse)}
  3. *
  4. {@link #renderTopNavigation(HttpServletRequest, PrintWriter)}
  5. *
  6. {@link #renderContent(HttpServletRequest, HttpServletResponse)}
  7. *
  8. {@link #endResponse(PrintWriter)}
  9. *
*

* Note: 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. *

* If the {@link #isHtmlRequest(HttpServletRequest)} method returns * false 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( "

" ); renderContent( request, response ); pw.println( "
" ); // 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 * true 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 true if the page should have headers and footers rendered */ protected boolean isHtmlRequest( final HttpServletRequest request ) { return true; } //---------- AbstractWebConsolePlugin API ---------------------------------- /** * 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 */ public 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; } /** * 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; /** * Retrieves the label. This is the last component in the servlet path. * * This method MUST be overridden, if the {@link #AbstractWebConsolePlugin()} * constructor is used. * * @return the label. */ public abstract String getLabel(); /** * Retrieves the title of the plug-in. It is displayed in the page header * and is also included in the title of the HTML document. * * This method MUST be overridden, if the {@link #AbstractWebConsolePlugin()} * constructor is used. * * @return the plugin title. */ public abstract String getTitle(); /** * Returns a list of CSS reference paths or null if no * additional CSS files are provided by the plugin. *

* The result is an array of strings which are used as the value of * the href attribute of the <link> elements * placed in the head section of the HTML generated. If the reference is * a relative path, it is turned into an absolute path by prepending the * value of the {@link WebConsoleConstants#ATTR_APP_ROOT} request attribute. * * @return The list of additional CSS files to reference in the head * section or null if no such CSS files are required. */ protected String[] getCssReferences() { return null; } /** * Returns the BundleContext with which this plugin has been * activated. If the plugin has not be activated by calling the * {@link #activate(BundleContext)} method, this method returns * null. * * @return the bundle context or null if the bundle is not activated. */ protected BundleContext getBundleContext() { return bundleContext; } /** * Returns the Bundle 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 * null. * * @return the bundle or null if the plugin is not activated. */ public final Bundle getBundle() { final BundleContext bundleContext = getBundleContext(); return ( bundleContext != null ) ? bundleContext.getBundle() : null; } /** * Returns the object which might provide resources. The class of this * object is used to find the getResource method. *

* This method may be overwritten by extensions. This base class * implementation returns this instance. * * @return The resource provider object or null if no * resources will be provided by this plugin. */ protected Object getResourceProvider() { return this; } /** * Returns a method which is called on the * {@link #getResourceProvider() resource provider} class to return an URL * to a resource which may be spooled when requested. The method has the * following signature: *

     * [modifier] URL getResource(String path);
     * 
* Where the [modifier] may be public, protected * or private (if the method is declared in the class of the * resource provider). It is suggested to use the private * modifier if the method is declared in the resource provider class or * the protected modifier if the method is declared in a * base class of the resource provider. * * @return The getResource(String) method or null * if the {@link #getResourceProvider() resource provider} is * null or does not provide such a method. */ private final Method getGetResourceMethod() { // return what we know of the getResourceMethod, if we already checked if (getResourceMethodChecked) { return getResourceMethod; } Method tmpGetResourceMethod = null; Object resourceProvider = getResourceProvider(); if ( resourceProvider != null ) { try { Class cl = resourceProvider.getClass(); while ( tmpGetResourceMethod == null && cl != Object.class ) { Method[] methods = cl.getDeclaredMethods(); for ( int i = 0; i < methods.length; i++ ) { Method m = methods[i]; if ( GET_RESOURCE_METHOD_NAME.equals( m.getName() ) && m.getParameterTypes().length == 1 && m.getParameterTypes()[0] == String.class && m.getReturnType() == URL.class ) { // ensure modifier is protected or public or the private // method is defined in the plugin class itself int mod = m.getModifiers(); if ( Modifier.isProtected( mod ) || Modifier.isPublic( mod ) || ( Modifier.isPrivate( mod ) && cl == resourceProvider.getClass() ) ) { m.setAccessible( true ); tmpGetResourceMethod = m; break; } } } cl = cl.getSuperclass(); } } catch ( Throwable t ) { tmpGetResourceMethod = null; } } // set what we have found and prevent future lookups getResourceMethod = tmpGetResourceMethod; getResourceMethodChecked = true; // now also return the method return getResourceMethod; } /** * Logs the message in the level * * @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 ) { log(level, message, null); } /** * Logs the message in the level * * @param level The log level at which to log the message * @param message The message to log * @param t The Throwable to log with the message */ public void log(final int level, final String message, final Throwable t ) { final String text = "[".concat(this.getTitle()).concat("] ").concat(message); switch(level) { case LogService.LOG_DEBUG: Util.LOGGER.debug(text, t); break; case LogService.LOG_INFO: Util.LOGGER.info(text, t); break; case LogService.LOG_WARNING: Util.LOGGER.warn(text, t); break; case LogService.LOG_ERROR: Util.LOGGER.error(text, t); break; default: Util.LOGGER.debug(message, t); } } /** * If the request addresses a resource which may be served by the * getResource method of the * {@link #getResourceProvider() resource provider}, this method serves it * and returns true. Otherwise false is returned. * false is also returned if the resource provider has no * getResource method. *

* If true 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 true if the request causes a resource to be sent back. * * @throws IOException If an error occurs accessing or spooling the resource. */ private final 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() { public Boolean run() throws Exception { return spoolResource0(request, response) ? Boolean.TRUE : Boolean.FALSE; } }); return ret.booleanValue(); } catch (PrivilegedActionException e) { final Exception x = e.getException(); throw x instanceof IOException ? (IOException) x : new IOException( x.toString()); } } final boolean spoolResource0( HttpServletRequest request, HttpServletResponse response ) throws IOException { // no resource if no resource accessor Method getResourceMethod = getGetResourceMethod(); if ( getResourceMethod == null ) { return false; } String pi = request.getPathInfo(); try { // check for a resource, fail if none URL url = ( URL ) getResourceMethod.invoke( getResourceProvider(), new Object[] { 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 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 OutputStream out = response.getOutputStream(); byte[] buf = new byte[2048]; int rd; while ( ( rd = ins.read( buf ) ) >= 0 ) { out.write( buf, 0, rd ); } // over and out ... return true; } } catch ( IllegalAccessException | InvocationTargetException ignore ) { // log or throw ??? } return false; } /** * 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) */ protected 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( WebConsoleConstants.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( NavigationRenderer.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" }) protected void renderTopNavigation(final HttpServletRequest request, final PrintWriter pw ) { final String appRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_APP_ROOT ); final Map menuMap = ( Map ) request.getAttribute( AbstractOsgiManagerPlugin.ATTR_LABEL_MAP_CATEGORIZED ); final Map langMap = (Map) request.getAttribute(WebConsoleConstants.ATTR_LANG_MAP); NavigationRenderer.renderTopNavigation(pw, appRoot, menuMap, langMap, request.getLocale()); } @SuppressWarnings({ "rawtypes" }) protected void renderMenu(final Map menuMap, final String appRoot, final PrintWriter pw ) { throw new UnsupportedOperationException(); } /** * 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(NavigationRenderer.FOOTER); } /** * Do not use this method anymore. Use the Servlet API for request parameter * handling. * @param request The request object * @param name The name of the parameter * @return The parameter value or null if the parameter is not set * @deprecated Use the Servlet API for uploads */ @Deprecated public static final String getParameter( HttpServletRequest request, String name ) { return WebConsoleUtil.getParameter(request, name); } /** * Utility method to handle relative redirects. * Some application servers like Web Sphere handle relative redirects differently * therefore we should make an absolute URL before invoking send redirect. * * @param request the HTTP request coming from the user * @param response the HTTP response, where data is rendered * @param redirectUrl the redirect URI. * @throws IOException If an input or output exception occurs * @throws IllegalStateException If the response was committed or if a partial * URL is given and cannot be converted into a valid URL * @deprecated use {@link WebConsoleUtil#sendRedirect(HttpServletRequest, HttpServletResponse, String)} */ protected void sendRedirect(final HttpServletRequest request, final HttpServletResponse response, String redirectUrl) throws IOException { WebConsoleUtil.sendRedirect(request, response, redirectUrl); } /** * Returns the {@link BrandingPlugin} currently used for web console * branding. * * @return the brandingPlugin * @deprecated */ @Deprecated public static BrandingPlugin getBrandingPlugin() { return AbstractWebConsolePlugin.BRANDING_PLUGIN; } /** * Sets the {@link BrandingPlugin} to use globally by all extensions of * this class for branding. *

* 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){ AbstractWebConsolePlugin.BRANDING_PLUGIN = DefaultBrandingPlugin.getInstance(); } else { AbstractWebConsolePlugin.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. *

* 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 ) { // nothing to do } /** * Reads the templateFile as a resource through the class * loader of this class converting the binary data into a string using * UTF-8 encoding. *

* 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 templateFile is * null * @throws RuntimeException if an IOException 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 ) { try { return Util.readTemplateFile( getClass(), templateFile ); } catch (final IOException e) { Util.LOGGER.error("readTemplateFile: File '{}' not found through class {}", templateFile, getClass() ); return ""; } } private final String getCssLinks( final String appRoot ) { // get the CSS references and return nothing if there are none final String[] cssRefs = getCssReferences(); if ( cssRefs == null ) { return ""; } // build the CSS links from the references final StringBuilder buf = new StringBuilder(); for ( int i = 0; i < cssRefs.length; i++ ) { buf.append( "" ); } return buf.toString(); } /** * If the url 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 url 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 url is null. */ private static final String toUrl( final String url, final String appRoot ) { if ( url.startsWith( "/" ) ) { return appRoot + url; } return url; } /** * Returns the {@link RequestVariableResolver} for the given request. *

* The resolver is added to the request attributes via the web console main * servlet before it invokes any plugins. * The preset properties are * appRoot set to the value of the * {@link WebConsoleConstants#ATTR_APP_ROOT} request attribute and * pluginRoot set to the value of the * {@link WebConsoleConstants#ATTR_PLUGIN_ROOT} request attribute. *

* * @param request The request whose attribute is returned * * @return The {@link RequestVariableResolver} for the given request. * @since 3.5.0 */ public RequestVariableResolver getVariableResolver( final ServletRequest request) { return (RequestVariableResolver) request.getAttribute( RequestVariableResolver.REQUEST_ATTRIBUTE ); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy