Java tutorial
/* * 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.text.MessageFormat; import java.util.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.commons.io.IOUtils; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; /** * 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"; /** * 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 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(); } /** * 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. * @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</true> but has been overwritten in the * {@link WebConsolePluginAdapter} for the plugins. * * @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><link></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>GenericServlet.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. * * @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) { log(message); } } /** * Calls the <code>GenericServlet.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) { log(message, t); } } /** * 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(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"); 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 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"); 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) + "}"; } String header = MessageFormat.format(getHeader(), new Object[] { brandingPlugin.getBrandName(), title, appRoot, getLabel(), toUrl(brandingPlugin.getFavIcon(), appRoot), toUrl(brandingPlugin.getMainStyleSheet(), appRoot), brandingPlugin.getProductURL(), brandingPlugin.getProductName(), toUrl(brandingPlugin.getProductImage(), appRoot), getCssLinks(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 */ protected void renderTopNavigation(HttpServletRequest request, PrintWriter pw) { // assume pathInfo to not be null, else this would not be called boolean linkToCurrent = true; String current = request.getPathInfo(); int slash = current.indexOf("/", 1); if (slash < 0) { slash = current.length(); linkToCurrent = false; } current = current.substring(1, slash); boolean disabled = false; String appRoot = (String) request.getAttribute(WebConsoleConstants.ATTR_APP_ROOT); Map labelMap = (Map) request.getAttribute(WebConsoleConstants.ATTR_LABEL_MAP); if (labelMap != null) { // prepare the navigation SortedMap map = new TreeMap(String.CASE_INSENSITIVE_ORDER); for (Iterator ri = labelMap.entrySet().iterator(); ri.hasNext();) { Map.Entry labelMapEntry = (Map.Entry) ri.next(); if (labelMapEntry.getKey() == null) { // ignore renders without a label } else if (disabled || current.equals(labelMapEntry.getKey())) { if (linkToCurrent) { map.put(labelMapEntry.getValue(), "<div class='ui-state-active'><a href='" + appRoot + "/" + labelMapEntry.getKey() + "'>" + labelMapEntry.getValue() + "</a></div>"); } else { map.put(labelMapEntry.getValue(), "<div class='ui-state-active'><span>" + labelMapEntry.getValue() + "</span></div>"); } } else { map.put(labelMapEntry.getValue(), "<div class='ui-state-default'><a href='" + appRoot + "/" + labelMapEntry.getKey() + "'>" + labelMapEntry.getValue() + "</a></div>"); } } // render the navigation pw.println("<div id='technav' class='ui-widget ui-widget-header'>"); for (Iterator li = map.values().iterator(); li.hasNext();) { pw.print(' '); pw.println(li.next()); } pw.println("</div>"); } // render lang-box Map langMap = (Map) request.getAttribute(WebConsoleConstants.ATTR_LANG_MAP); if (null != langMap && !langMap.isEmpty()) { pw.println("<div id='langSelect'>"); //$NON-NLS-1$ pw.println(" <span class='ui-icon ui-icon-comment'> </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(); pw.print(" <img src='"); //$NON-NLS-1$ pw.print(appRoot); pw.print("/res/flags/"); //$NON-NLS-1$ pw.print(l); pw.print(".gif' alt='"); //$NON-NLS-1$ pw.print(l); pw.print("' title='"); //$NON-NLS-1$ pw.print(langMap.get(l)); pw.println("'/>"); //$NON-NLS-1$ } pw.println(" </span>"); //$NON-NLS-1$ pw.println("</div>"); //$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"); } return HEADER; } private final String getFooter() { if (FOOTER == null) { FOOTER = readTemplateFile(AbstractWebConsolePlugin.class, "/templates/main_footer.html"); } return FOOTER; } /** * Reads the <code>templateFile</code> as a resource through the class * loader of this class converting the binary data into a string using * 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"); 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); } finally { IOUtils.closeQuietly(templateStream); } } // template file does not exist, return an empty string log("readTemplateFile: File '" + templateFile + "' not found through class " + clazz); return ""; } private final String getCssLinks(final String appRoot) { // get the CSS references and return nothing if there are none final String[] cssRefs = getCssReferences(); if (cssRefs == null) { return ""; } // build the CSS links from the references final StringBuffer buf = new StringBuffer(); for (int i = 0; i < cssRefs.length; i++) { buf.append("<link href='"); buf.append(toUrl(cssRefs[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; } }