blob: ab9b7f6f8aad406bc38890240846f9d4b32bb13b [file] [log] [blame]
/*
* 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.*;
import java.lang.reflect.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.*;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import org.apache.commons.io.IOUtils;
import org.apache.felix.webconsole.internal.servlet.OsgiManager;
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
* <code>felix.webconsole.label</code> 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.
*/
public abstract class AbstractWebConsolePlugin extends HttpServlet
{
/** Pseudo class version ID to keep the IDE quite. */
private static final long serialVersionUID = 1L;
/** The name of the request attribute containing the map of FileItems from the POST request */
public static final String ATTR_FILEUPLOAD = "org.apache.felix.webconsole.fileupload"; //$NON-NLS-1$
/**
* The name of the request attribute containing a {@link java.io.File} - upload repository path used by
* {@link org.apache.commons.fileupload.disk.DiskFileItemFactory}.<p>
*
* The Web Console plugin, that utilizes file upload capabilities of the web console SHOULD:
* <ol>
* <li>Obtain the file using {@link org.osgi.framework.BundleContext#getDataFile(String)}
* <li>Set the file as request attribute
* <li>Use {@link WebConsoleUtil#getParameter(HttpServletRequest, String)} to obtain the file(s)
* </ol>
*
* Without setting this attribute, your plugin will not work if there is a security manager enabled.
* It is guaranteed, that your plugin has permissions to read/write/delete files to the location,
* provided by the bundle context.
*/
public static final String ATTR_FILEUPLOAD_REPO = "org.apache.felix.webconsole.fileupload.repo"; //$NON-NLS-1$
/**
* 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"; //$NON-NLS-1$
/**
* The header fragment read from the templates/main_header.html file
*/
private static String HEADER;
/**
* The footer fragment read from the templates/main_footer.html file
*/
private static String FOOTER;
/**
* The reference to the getResource method provided by the
* {@link #getResourceProvider()}. This is <code>null</code> if there is
* 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 BrandingPlugin brandingPlugin = DefaultBrandingPlugin.getInstance();
private static int logLevel;
//---------- 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
* <code>felix.webconsole.category</code> 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:
* <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'>" ); //$NON-NLS-1$
renderContent( request, response );
pw.println( "</div>" ); //$NON-NLS-1$
// 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;
}
//---------- 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 <code>null</code> if no
* additional CSS files are provided by the plugin.
* <p>
* The result is an array of strings which are used as the value of
* the <code>href</code> attribute of the <code>&lt;link&gt;</code> 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 <code>null</code> if no such CSS files are required.
*/
protected String[] getCssReferences()
{
return 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.
*/
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 <code>getResource</code> method.
* <p>
* This method may be overwritten by extensions. This base class
* implementation returns this instance.
*
* @return The resource provider object or <code>null</code> 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:
* <pre>
* [modifier] URL getResource(String path);
* </pre>
* Where the <i>[modifier]</i> may be <code>public</code>, <code>protected</code>
* or <code>private</code> (if the method is declared in the class of the
* resource provider). It is suggested to use the <code>private</code>
* modifier if the method is declared in the resource provider class or
* the <code>protected</code> modifier if the method is declared in a
* base class of the resource provider.
*
* @return The <code>getResource(String)</code> method or <code>null</code>
* if the {@link #getResourceProvider() resource provider} is
* <code>null</code> 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;
}
/**
* 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 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 = (Boolean) AccessController.doPrivileged(new PrivilegedExceptionAction()
{
public Object 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();
InputStream ins = null;
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();
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" ); //$NON-NLS-1$
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 ); //$NON-NLS-1$
}
// describe the contents
response.setContentType( getServletContext().getMimeType( pi ) );
response.setIntHeader( "Content-Length", connection.getContentLength() ); //$NON-NLS-1$
// 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 iae )
{
// log or throw ???
}
catch ( InvocationTargetException ite )
{
// log or throw ???
// Throwable cause = ite.getTargetException();
}
finally
{
IOUtils.closeQuietly(ins);
}
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" ); //$NON-NLS-1$
response.setContentType( "text/html" ); //$NON-NLS-1$
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( "%" ) ) //$NON-NLS-1$
{
title = "${" + title.substring( 1 ) + "}"; //$NON-NLS-1$ //$NON-NLS-2$
}
VariableResolver resolver = WebConsoleUtil.getVariableResolver(request);
if (resolver instanceof DefaultVariableResolver) {
DefaultVariableResolver r = (DefaultVariableResolver) resolver;
r.put("head.title", title); //$NON-NLS-1$
r.put("head.label", getLabel()); //$NON-NLS-1$
r.put("head.cssLinks", getCssLinks(appRoot)); //$NON-NLS-1$
r.put("brand.name", brandingPlugin.getBrandName()); //$NON-NLS-1$
r.put("brand.product.url", brandingPlugin.getProductURL()); //$NON-NLS-1$
r.put("brand.product.name", brandingPlugin.getProductName()); //$NON-NLS-1$
r.put("brand.product.img", toUrl( brandingPlugin.getProductImage(), appRoot )); //$NON-NLS-1$
r.put("brand.favicon", toUrl( brandingPlugin.getFavIcon(), appRoot )); //$NON-NLS-1$
r.put("brand.css", toUrl( brandingPlugin.getMainStyleSheet(), appRoot )); //$NON-NLS-1$
}
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
*/
protected void renderTopNavigation( HttpServletRequest request, PrintWriter pw )
{
// assume pathInfo to not be null, else this would not be called
String current = request.getPathInfo();
int slash = current.indexOf( "/", 1 ); //$NON-NLS-1$
if ( slash < 0 )
{
slash = current.length();
}
current = current.substring( 1, slash );
String appRoot = ( String ) request.getAttribute( WebConsoleConstants.ATTR_APP_ROOT );
Map menuMap = ( Map ) request.getAttribute( OsgiManager.ATTR_LABEL_MAP_CATEGORIZED );
this.renderMenu( menuMap, appRoot, pw );
// render lang-box
Map langMap = (Map) request.getAttribute(WebConsoleConstants.ATTR_LANG_MAP);
if (null != langMap && !langMap.isEmpty())
{
// determine the currently selected locale from the request and fail-back
// to the default locale if not set
// if locale is missing in locale map, the default 'en' locale is used
Locale reqLocale = request.getLocale();
String locale = null != reqLocale ? reqLocale.getLanguage()
: Locale.getDefault().getLanguage();
if (!langMap.containsKey(locale))
{
locale = Locale.getDefault().getLanguage();
}
if (!langMap.containsKey(locale))
{
locale = "en"; //$NON-NLS-1$
}
pw.println("<div id='langSelect'>"); //$NON-NLS-1$
pw.println(" <span>"); //$NON-NLS-1$
printLocaleElement(pw, appRoot, locale, langMap.get(locale));
pw.println(" </span>"); //$NON-NLS-1$
pw.println(" <span class='flags ui-helper-hidden'>"); //$NON-NLS-1$
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>"); //$NON-NLS-1$
pw.println("</div>"); //$NON-NLS-1$
}
}
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>" );
}
}
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>" );
}
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='"); //$NON-NLS-1$
pw.print(appRoot);
pw.print("/res/flags/"); //$NON-NLS-1$
pw.print(langCode);
pw.print(".gif' alt='"); //$NON-NLS-1$
pw.print(langCode);
pw.print("' title='"); //$NON-NLS-1$
pw.print(langName);
pw.println("'/>"); //$NON-NLS-1$
}
/**
* 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());
}
/**
* An utility method, that is used to filter out simple parameter from file
* parameter when multipart transfer encoding is used.
*
* This method processes the request and sets a request attribute
* {@link #ATTR_FILEUPLOAD}. The attribute value is a {@link Map}
* where the key is a String specifying the field name and the value
* is a {@link org.apache.commons.fileupload.FileItem}.
*
* @param request the HTTP request coming from the user
* @param name the name of the parameter
* @return if not multipart transfer encoding is used - the value is the
* parameter value or <code>null</code> if not set. If multipart is used,
* and the specified parameter is field - then the value of the parameter
* is returned.
* @deprecated use {@link WebConsoleUtil#getParameter(HttpServletRequest, String)}
*/
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
*/
public static BrandingPlugin getBrandingPlugin() {
return AbstractWebConsolePlugin.brandingPlugin;
}
/**
* 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(BrandingPlugin brandingPlugin) {
if(brandingPlugin == null){
AbstractWebConsolePlugin.brandingPlugin = DefaultBrandingPlugin.getInstance();
} else {
AbstractWebConsolePlugin.brandingPlugin = 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( int logLevel )
{
AbstractWebConsolePlugin.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( AbstractWebConsolePlugin.class, "/templates/main_header.html" ); //$NON-NLS-1$
}
return HEADER;
}
private final String getFooter()
{
if ( FOOTER == null )
{
FOOTER = readTemplateFile( AbstractWebConsolePlugin.class, "/templates/main_footer.html" ); //$NON-NLS-1$
}
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 );
}
private final String readTemplateFile( final Class clazz, final String templateFile)
{
InputStream templateStream = clazz.getResourceAsStream( templateFile );
if ( templateStream != null )
{
try
{
String str = IOUtils.toString( templateStream, "UTF-8" ); //$NON-NLS-1$
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 ); //$NON-NLS-1$ //$NON-NLS-2$
}
finally
{
IOUtils.closeQuietly( templateStream );
}
}
// template file does not exist, return an empty string
log( LogService.LOG_ERROR, "readTemplateFile: File '" + templateFile + "' not found through class " + clazz ); //$NON-NLS-1$ //$NON-NLS-2$
return ""; //$NON-NLS-1$
}
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 ""; //$NON-NLS-1$
}
// build the CSS links from the references
final StringBuffer buf = new StringBuffer();
for ( int i = 0; i < cssRefs.length; i++ )
{
buf.append( "<link href='" ); //$NON-NLS-1$
buf.append( toUrl( cssRefs[i], appRoot ) );
buf.append( "' rel='stylesheet' type='text/css' />" ); //$NON-NLS-1$
}
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( "/" ) ) //$NON-NLS-1$
{
return appRoot + url;
}
return url;
}
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;
}
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;
}
}
}