FELIX-3874 :  Create new status printer module

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1439568 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/status-printer/pom.xml b/status-printer/pom.xml
new file mode 100644
index 0000000..a2e76b0
--- /dev/null
+++ b/status-printer/pom.xml
@@ -0,0 +1,101 @@
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>felix-parent</artifactId>
+        <version>2.1</version>
+        <relativePath>../pom/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.felix.status</artifactId>
+    <packaging>bundle</packaging>
+    <version>0.0.1-SNAPSHOT</version>
+
+    <name>Apache Felix Status</name>
+    <description>
+    Status Provider
+    </description>
+    
+    <build>
+        <directory>${bundle.build.name}</directory>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <version>2.3.7</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-Category>osgi</Bundle-Category>
+                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                        <Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+                        <DynamicImport-Package>javax.servlet, javax.servlet.http</DynamicImport-Package>
+                        <Bundle-Activator>org.apache.felix.status.impl.Activator</Bundle-Activator>
+                    </instructions>
+                </configuration>
+            </plugin>
+            <!--
+                Configure default compilation for Java 5
+            -->
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>1.5</source>
+                    <target>1.5</target>
+                </configuration>
+            </plugin>
+        </plugins>        
+    </build>    
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>4.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>4.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.5.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>biz.aQute</groupId>
+            <artifactId>bndlib</artifactId>
+            <version>1.43.0</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/status-printer/src/main/java/org/apache/felix/status/PrinterMode.java b/status-printer/src/main/java/org/apache/felix/status/PrinterMode.java
new file mode 100644
index 0000000..40250f2
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/PrinterMode.java
@@ -0,0 +1,30 @@
+/*
+ * 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.status;
+
+/**
+ * Enumeration for the different printer modes.
+ */
+public enum PrinterMode {
+
+    TEXT,       // plain text
+    HTML_BODY,  // HTML which can be placed inside a HTML body element (no external references)
+    JSON,       // JSON output
+    ZIP_FILE    // file content for a zip
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/StatusPrinter.java b/status-printer/src/main/java/org/apache/felix/status/StatusPrinter.java
new file mode 100644
index 0000000..d4b446e
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/StatusPrinter.java
@@ -0,0 +1,86 @@
+/*
+ * 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.status;
+
+import java.io.PrintWriter;
+
+/**
+ * The <code>StatusPrinter</code> is a service interface to be
+ * implemented by providers which want to hook into the display of the
+ * current configuration and state of the OSGi framework and application.
+ *
+ * A status printer must configure at least these three configuration properties
+ * <ul>
+ *   <li>{@link #CONFIG_PRINTER_MODES} - the supported modes</li>
+ *   <li>{@link #CONFIG_TITLE} - the printer title</li>
+ *   <li>{@link #CONFIG_NAME} - the printer name</li>
+ * </ul>
+ */
+public interface StatusPrinter {
+
+    /**
+     * The service name under which services of this class must be registered
+     * to be picked for inclusion in the configuration report.
+     */
+    String SERVICE = StatusPrinter.class.getName(); //$NON-NLS-1$
+
+    /**
+     * The property defining the supported rendering modes.
+     * The value of this property is either a string or a string array containing
+     * valid names of {@link PrinterMode}.
+     *
+     * If this property is missing or contains invalid values,
+     * the printer is ignored.
+     */
+    String CONFIG_PRINTER_MODES = "felix.statusprinter.modes"; //$NON-NLS-1$
+
+    /**
+     * The unique name of the printer.
+     * If this property is missing the printer is ignored.
+     * If there are two or more services with the same name, the
+     * services with the highest ranking is used.
+     */
+    String CONFIG_NAME = "felix.statusprinter.name"; //$NON-NLS-1$
+
+    /**
+     * The title displayed by tools when this printer is used. It should be
+     * descriptive but short.
+     * If this property is missing the printer is ignored.
+     */
+    String CONFIG_TITLE = "felix.statusprinter.title"; //$NON-NLS-1$
+
+    /**
+     * The category under which this printer is categorized.
+     * This property is optional.
+     */
+    String CONFIG_CATEGORY = "felix.statusprinter.category"; //$NON-NLS-1$
+
+    /**
+     * Prints the configuration report to the given <code>printWriter</code>.
+     * Implementations are free to print whatever information they deem useful.
+     *
+     * If a printer is invoked with a mode it doesn't support ({@link #CONFIG_PRINTER_MODES})
+     * the printer should just do/print nothing and directly return.
+     *
+     * @param mode The render mode.
+     * @param printWriter where to write the configuration data. It might be flushed,
+     * but must not be closed.
+     */
+    void print( PrinterMode mode, PrintWriter printWriter );
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/StatusPrinterHandler.java b/status-printer/src/main/java/org/apache/felix/status/StatusPrinterHandler.java
new file mode 100644
index 0000000..18bde07
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/StatusPrinterHandler.java
@@ -0,0 +1,46 @@
+/*
+ * 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.status;
+
+
+/**
+ * The status printer handler can be used by clients to access
+ * a status printer. The handlers can be get from the {@link StatusPrinterManager}.
+ *
+ * For clients using status printers, a handler simplifies accessing and
+ * working with the status printer. A client should never lookup a
+ * status printer directly.
+ */
+public interface StatusPrinterHandler extends StatusPrinter, ZipAttachmentProvider {
+
+    /** The unique name of the printer. */
+    String getName();
+
+    /** The human readable title for the status printer. */
+    String getTitle();
+
+    /** The optional category for this printer. */
+    String getCategory();
+
+    /** All supported modes. */
+    PrinterMode[] getModes();
+
+    /** Whether the printer supports this mode. */
+    boolean supports( final PrinterMode mode );
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/StatusPrinterManager.java b/status-printer/src/main/java/org/apache/felix/status/StatusPrinterManager.java
new file mode 100644
index 0000000..617f5ce
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/StatusPrinterManager.java
@@ -0,0 +1,46 @@
+/*
+ * 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.status;
+
+/**
+ * The manager allows to access status printers.
+ * Instead of directly returning a status printer, a status
+ * printer handler is returned which provides access to the
+ * meta information of the printer and other utility methods.
+ */
+public interface StatusPrinterManager {
+
+    /**
+     * Get all status printer handlers.
+     * @return A list of handlers - might be empty.
+     */
+    StatusPrinterHandler[] getAllHandlers();
+
+    /**
+     * Get all handlers supporting the mode.
+     * @return A list of handlers - might be empty.
+     */
+    StatusPrinterHandler[] getHandlers( final PrinterMode mode);
+
+    /**
+     * Return a handler for the unique name.
+     * @return The corresponding handler or <code>null</code>.
+     */
+    StatusPrinterHandler getHandler( final String name );
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/ZipAttachmentProvider.java b/status-printer/src/main/java/org/apache/felix/status/ZipAttachmentProvider.java
new file mode 100644
index 0000000..37cb6b8
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/ZipAttachmentProvider.java
@@ -0,0 +1,53 @@
+/*
+ * 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.status;
+
+import java.io.IOException;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * This is an optional extension of the {@link StatusPrinter}.
+ * If a status printer implements this interface, the printer
+ * can add additional attachments to the output of the
+ * configuration zip.
+ *
+ * A service implementing this method must still register itself
+ * as a {@link StatusPrinter} but not as a
+ * {@link ZipAttachmentProvider} service.
+ */
+public interface ZipAttachmentProvider extends StatusPrinter {
+
+    /**
+     * Add attachments to the zip output stream.
+     * The attachment provider can add as many attachments in any format
+     * as it wants. However it should use the namePrefix to create unique
+     * names / paths inside the zip.
+     *
+     * The general pattern is: creating a zip entry by using the name prefix
+     * and a name, adding the entry to the zip output stream, writing
+     * the content of the file to the stream, and finally ending the
+     * zip entry.
+     *
+     * @param namePrefix Name prefix to use for zip entries. Ends with a slash.
+     * @param zos The zip output stream.
+     * @throws IOException
+     */
+    void addAttachments(final String namePrefix, final ZipOutputStream zos)
+    throws IOException;
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/AbstractWebConsolePlugin.java b/status-printer/src/main/java/org/apache/felix/status/impl/AbstractWebConsolePlugin.java
new file mode 100644
index 0000000..3b62da8
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/AbstractWebConsolePlugin.java
@@ -0,0 +1,447 @@
+/*
+ * 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.status.impl;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.StatusPrinterHandler;
+import org.apache.felix.status.StatusPrinterManager;
+
+/**
+ * The web console plugin for a status printer.
+ */
+public abstract class AbstractWebConsolePlugin extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    /** The status printer manager. */
+    protected final StatusPrinterManager statusPrinterManager;
+
+    /**
+     * Constructor
+     * @param statusPrinterManager The manager
+     */
+    AbstractWebConsolePlugin(final StatusPrinterManager statusPrinterManager) {
+        this.statusPrinterManager = statusPrinterManager;
+    }
+
+    protected abstract StatusPrinterHandler getStatusPrinterHandler();
+
+    private void printConfigurationStatus( final ConfigurationWriter pw,
+            final PrinterMode mode,
+            final StatusPrinterHandler handler )
+    throws IOException {
+        if ( handler == null ) {
+            for(final StatusPrinterHandler sph : this.statusPrinterManager.getHandlers(mode)) {
+                pw.printStatus(mode, sph);
+            }
+        } else {
+            if ( handler.supports(mode) ) {
+                pw.printStatus(mode, handler);
+            }
+        }
+    }
+
+    /**
+     * Sets response headers to force the client to not cache the response
+     * sent back. This method must be called before the response is committed
+     * otherwise it will have no effect.
+     * <p>
+     * This method sets the <code>Cache-Control</code>, <code>Expires</code>,
+     * and <code>Pragma</code> headers.
+     *
+     * @param response The response for which to set the cache prevention
+     */
+    private final void setNoCache(final HttpServletResponse response) {
+        response.setHeader("Cache-Control", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
+        response.addHeader("Cache-Control", "no-store"); //$NON-NLS-1$ //$NON-NLS-2$
+        response.addHeader("Cache-Control", "must-revalidate"); //$NON-NLS-1$ //$NON-NLS-2$
+        response.addHeader("Cache-Control", "max-age=0"); //$NON-NLS-1$ //$NON-NLS-2$
+        response.setHeader("Expires", "Thu, 01 Jan 1970 01:00:00 GMT"); //$NON-NLS-1$ //$NON-NLS-2$
+        response.setHeader("Pragma", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest request,
+            final HttpServletResponse response)
+    throws ServletException, IOException {
+        this.setNoCache( response );
+
+        // full request?
+        final StatusPrinterHandler handler;
+        if ( request.getPathInfo().lastIndexOf('/') > 0 ) {
+            handler = null; // all;
+        } else {
+            handler = this.getStatusPrinterHandler();
+            if ( handler == null ) {
+                response.sendError( HttpServletResponse.SC_NOT_FOUND );
+                return;
+            }
+        }
+
+        if ( request.getPathInfo().endsWith( ".txt" ) ) { //$NON-NLS-2$
+            response.setContentType( "text/plain; charset=utf-8" ); //$NON-NLS-2$
+            final ConfigurationWriter pw = new PlainTextConfigurationWriter( response.getWriter() );
+            printConfigurationStatus( pw, PrinterMode.TEXT, handler );
+            pw.flush();
+        } else if ( request.getPathInfo().endsWith( ".zip" ) ) { //$NON-NLS-2$
+            String type = getServletContext().getMimeType( request.getPathInfo() );
+            if ( type == null ) {
+                type = "application/x-zip"; //$NON-NLS-2$
+            }
+            response.setContentType( type );
+
+            final ZipOutputStream zip = new ZipOutputStream( response.getOutputStream() );
+            zip.setLevel( Deflater.BEST_SPEED );
+            zip.setMethod( ZipOutputStream.DEFLATED );
+
+            final Date now = new Date();
+            // create time stamp entry
+            final ZipEntry entry = new ZipEntry( "timestamp.txt" ); //$NON-NLS-2$
+            entry.setTime(now.getTime());
+            zip.putNextEntry( entry );
+            final StringBuilder sb = new StringBuilder();
+            sb.append("Date: ");
+            synchronized ( StatusPrinterAdapter.DISPLAY_DATE_FORMAT )                             {
+                sb.append(StatusPrinterAdapter.DISPLAY_DATE_FORMAT.format(now));
+            }
+            sb.append(" (");
+            sb.append(String.valueOf(now.getTime()));
+            sb.append(")\n");
+
+            zip.write(sb.toString().getBytes("UTF-8"));
+            zip.closeEntry();
+
+            final ZipConfigurationWriter pw = new ZipConfigurationWriter( zip );
+            printConfigurationStatus( pw, PrinterMode.ZIP_FILE, handler );
+
+            zip.finish();
+        } else if ( request.getPathInfo().endsWith( ".nfo" ) ) {
+            if ( handler == null ) {
+                response.sendError( HttpServletResponse.SC_NOT_FOUND);
+                return;
+            }
+            response.setContentType( "text/html; charset=utf-8" );
+
+            final HtmlConfigurationWriter pw = new HtmlConfigurationWriter( response.getWriter() );
+            pw.println ( "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"" );
+            pw.println ( "  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" );
+            pw.println ( "<html xmlns=\"http://www.w3.org/1999/xhtml\">" );
+            pw.println ( "<head><title>dummy</title></head><body><div>" );
+
+            if ( handler.supports(PrinterMode.HTML_BODY) ) {
+                handler.print(PrinterMode.HTML_BODY, pw);
+            } else {
+                pw.enableFilter( true );
+                handler.print(PrinterMode.TEXT, pw);
+                pw.enableFilter( false );
+            }
+            pw.println( "</div></body></html>" );
+            return;
+        } else {
+            if ( handler == null ) {
+                response.sendError( HttpServletResponse.SC_NOT_FOUND);
+                return;
+            }
+            final HtmlConfigurationWriter pw = new HtmlConfigurationWriter(response.getWriter());
+            pw.println("<script type=\"text/javascript\">");
+            pw.println("// <![CDATA[");
+            pw.println("function pad(value) { if ( value < 10 ) { return '0' + value;} return '' + value;}");
+            pw.println("function downloadDump(ext, full) {");
+            pw.println("  if (full) {");
+            pw.println("    var now = new Date();");
+            pw.println("    var name = \"configuration-status-\" + now.getUTCFullYear() + pad(now.getUTCMonth() + 1) + pad(now.getUTCDate()) + \"-\" + pad(now.getUTCHours()) + pad(now.getUTCMinutes()) + pad(now.getUTCSeconds()) + \".\";");
+            pw.println("    location.href = location.href + \"/\" + name + ext;");
+            pw.println("  } else {");
+            pw.println("    location.href = location.href + '.' + ext;");
+            pw.println("  }");
+            pw.println("}");
+
+            pw.println("$(document).ready(function() {");
+            pw.println("    $('.downloadTxt').click(function() { downloadDump('txt', false)});");
+            pw.println("    $('.downloadZip').click(function() { downloadDump('zip', false)});");
+            pw.println("    $('.downloadFullZip').click(function() { downloadDump('zip', true)});");
+            pw.println("    $('.downloadFullTxt').click(function() { downloadDump('txt', true)});");
+            pw.println("});");
+            pw.println("// ]]>");
+            pw.println("</script>");
+            pw.println( "<br/><p class=\"statline\">");
+
+            final Date currentTime = new Date();
+            synchronized ( StatusPrinterAdapter.DISPLAY_DATE_FORMAT )                             {
+                pw.print("Date: ");
+                pw.println(StatusPrinterAdapter.DISPLAY_DATE_FORMAT.format(currentTime));
+            }
+
+            pw.print("<button type=\"button\" class=\"downloadFullZip\" style=\"float: right; margin-right: 30px; margin-top: 5px;\">Download Full Zip</button>");
+            pw.print("<button type=\"button\" class=\"downloadFullTxt\" style=\"float: right; margin-right: 30px; margin-top: 5px;\">Download Full Text</button>");
+
+            if ( handler.supports(PrinterMode.ZIP_FILE) ) {
+                pw.print("<button type=\"button\" class=\"downloadZip\" style=\"float: right; margin-right: 30px; margin-top: 5px;\">Download As Zip</button>");
+            }
+            if ( handler.supports(PrinterMode.TEXT ) ) {
+                pw.print("<button type=\"button\" class=\"downloadTxt\" style=\"float: right; margin-right: 30px; margin-top: 5px;\">Download As Text</button>");
+            }
+
+            pw.println("<br/>&nbsp;</p>"); // status line
+            pw.print("<div>");
+            if ( handler.supports(PrinterMode.HTML_BODY) ) {
+                handler.print(PrinterMode.HTML_BODY, pw);
+            } else {
+                pw.enableFilter( true );
+                handler.print(PrinterMode.TEXT, pw);
+                pw.enableFilter( false );
+            }
+            pw.print("</div>");
+        }
+    }
+
+    /**
+     * Base class for all configuration writers.
+     */
+    private abstract static class ConfigurationWriter extends PrintWriter {
+
+        ConfigurationWriter( final Writer delegatee ) {
+            super( delegatee );
+        }
+
+        protected void title( final String title ) throws IOException {
+            // dummy implementation
+        }
+
+
+        protected void end() throws IOException {
+            // dummy implementation
+        }
+
+        public void printStatus(
+                final PrinterMode mode,
+                final StatusPrinterHandler handler)
+        throws IOException {
+            this.title(handler.getTitle());
+            handler.print(mode, this);
+            this.end();
+        }
+    }
+
+    /**
+     * The HTML configuration writer outputs the status as an HTML snippet.
+     */
+    private static class HtmlConfigurationWriter extends ConfigurationWriter {
+
+        // whether or not to filter "<" signs in the output
+        private boolean doFilter;
+
+
+        HtmlConfigurationWriter( final Writer delegatee ) {
+            super( delegatee );
+        }
+
+
+        void enableFilter( final boolean doFilter ) {
+            this.doFilter = doFilter;
+        }
+
+        // IE has an issue with white-space:pre in our case so, we write
+        // <br/> instead of [CR]LF to get the line break. This also works
+        // in other browsers.
+        @Override
+        public void println() {
+            if ( doFilter ) {
+                this.write('\n'); // write <br/>
+            } else {
+                super.println();
+            }
+        }
+
+        // some VM implementation directly write in underlying stream, instead of
+        // delegation to the write() method. So we need to override this, to make
+        // sure, that everything is escaped correctly
+        @Override
+        public void print(final String str) {
+            final char[] chars = str.toCharArray();
+            write(chars, 0, chars.length);
+        }
+
+
+        private final char[] oneChar = new char[1];
+
+        // always delegate to write(char[], int, int) otherwise in some VM
+        // it cause endless cycle and StackOverflowError
+        @Override
+        public void write(final int character) {
+            synchronized (oneChar) {
+                oneChar[0] = (char) character;
+                write(oneChar, 0, 1);
+            }
+        }
+
+        // write the characters unmodified unless filtering is enabled in
+        // which case the writeFiltered(String) method is called for filtering
+        @Override
+        public void write(char[] chars, int off, int len) {
+            if (doFilter) {
+                chars = this.escapeHtml(new String(chars, off, len)).toCharArray();
+                off = 0;
+                len = chars.length;
+            }
+            super.write(chars, off, len);
+        }
+
+        // write the string unmodified unless filtering is enabled in
+        // which case the writeFiltered(String) method is called for filtering
+        @Override
+        public void write( final String string, final int off, final int len ) {
+            write(string.toCharArray(), off, len);
+        }
+
+        /**
+         * Escapes HTML special chars like: <>&\r\n and space
+         *
+         *
+         * @param text the text to escape
+         * @return the escaped text
+         */
+        private String escapeHtml(final String text) {
+            final StringBuilder sb = new StringBuilder(text.length() * 4 / 3);
+            char ch, oldch = '_';
+            for (int i = 0; i < text.length(); i++) {
+                switch (ch = text.charAt(i)) {
+                    case '<':
+                        sb.append("&lt;"); //$NON-NLS-1$
+                        break;
+                    case '>':
+                        sb.append("&gt;"); //$NON-NLS-1$
+                        break;
+                    case '&':
+                        sb.append("&amp;"); //$NON-NLS-1$
+                        break;
+                    case ' ':
+                        sb.append("&nbsp;"); //$NON-NLS-1$
+                        break;
+                    case '\r':
+                    case '\n':
+                        if (oldch != '\r' && oldch != '\n') // don't add twice <br>
+                            sb.append("<br/>\n"); //$NON-NLS-1$
+                        break;
+                    default:
+                        sb.append(ch);
+                }
+                oldch = ch;
+            }
+
+            return sb.toString();
+        }
+    }
+
+    /**
+     * The plain text configuration writer outputs the status as plain text.
+     */
+    private static class PlainTextConfigurationWriter extends ConfigurationWriter {
+
+        PlainTextConfigurationWriter( final Writer delegatee ) {
+            super( delegatee );
+        }
+
+        @Override
+        protected void title( final String title ) throws IOException {
+            print( "*** " );
+            print( title );
+            println( ":" );
+        }
+
+
+        @Override
+        protected void end() throws IOException {
+            println();
+        }
+    }
+
+    /**
+     * The ZIP configuration writer creates a zip with
+     * - txt output of a status printers (if supported)
+     * - json output of a status printers (if supported)
+     * - attachments from a status printer (if supported)
+     */
+    private static class ZipConfigurationWriter extends ConfigurationWriter {
+
+        private final ZipOutputStream zip;
+
+        private int counter;
+
+        ZipConfigurationWriter( final ZipOutputStream zip ) {
+            super( new OutputStreamWriter( zip ) );
+            this.zip = zip;
+        }
+
+        private String getFormattedTitle(final String title) {
+            return MessageFormat.format( "{0,number,000}-{1}", new Object[]
+                    { new Integer( counter ), title } );
+        }
+
+        @Override
+        protected void title( final String title ) throws IOException {
+            counter++;
+
+            final String name = getFormattedTitle(title).concat(".txt");
+
+            final ZipEntry entry = new ZipEntry( name );
+            zip.putNextEntry( entry );
+        }
+
+        @Override
+        protected void end() throws IOException {
+            flush();
+
+            zip.closeEntry();
+        }
+
+        @Override
+        public void printStatus(
+                final PrinterMode mode,
+                final StatusPrinterHandler handler)
+        throws IOException {
+            super.printStatus(mode, handler);
+            final String title = getFormattedTitle(handler.getTitle());
+            handler.addAttachments(title.concat("/"), this.zip);
+            if ( handler.supports(PrinterMode.JSON) ) {
+                final String name = "json/".concat(title).concat(".json");
+
+                final ZipEntry entry = new ZipEntry( name );
+                zip.putNextEntry( entry );
+                handler.print(PrinterMode.JSON, this);
+                flush();
+
+                zip.closeEntry();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/Activator.java b/status-printer/src/main/java/org/apache/felix/status/impl/Activator.java
new file mode 100644
index 0000000..d43210d
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/Activator.java
@@ -0,0 +1,73 @@
+/*
+ * 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.status.impl;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.apache.felix.status.StatusPrinterManager;
+import org.apache.felix.status.impl.webconsole.WebConsoleAdapter;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * Activate bridges and register manager.
+ */
+public class Activator implements BundleActivator {
+
+    private StatusPrinterManagerImpl printerManager;
+
+    private ServiceRegistration managerRegistration;
+
+    private WebConsoleAdapter webAdapter;
+
+    /**
+     * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
+     */
+    public void start(final BundleContext context) throws Exception {
+        this.webAdapter = new WebConsoleAdapter(context);
+        this.printerManager = new StatusPrinterManagerImpl(context);
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(Constants.SERVICE_DESCRIPTION, "Apache Felix Status Printer Manager");
+        props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+        this.managerRegistration = context.registerService(
+                StatusPrinterManager.class.getName(),
+                this.printerManager, props);
+
+    }
+
+    /**
+     * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
+     */
+    public void stop(final BundleContext context) throws Exception {
+        if( this.managerRegistration != null ) {
+            this.managerRegistration.unregister();
+            this.managerRegistration = null;
+        }
+        if ( this.printerManager != null ) {
+            this.printerManager.dispose();
+            this.printerManager = null;
+        }
+        if ( this.webAdapter != null ) {
+            this.webAdapter.dispose();
+            this.webAdapter = null;
+        }
+    }
+
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/ClassUtils.java b/status-printer/src/main/java/org/apache/felix/status/impl/ClassUtils.java
new file mode 100644
index 0000000..6c02724
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/ClassUtils.java
@@ -0,0 +1,58 @@
+/*
+ * 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.status.impl;
+
+import java.lang.reflect.Method;
+
+/**
+ * Utility methods for dynamic method invocations
+ */
+public class ClassUtils {
+
+    /**
+     * Search a method with the given name and signature.
+     * @return The method or <code>null</code> if not found.
+     */
+    public static Method searchMethod(final Class<?> clazz, final String mName, final Class<?>[] params) {
+        try {
+            final Method m = clazz.getMethod(mName, params);
+            m.setAccessible(true);
+            return m;
+        } catch (Throwable nsme) {
+            // ignore, we catch Throwable above to not only catch NoSuchMethodException
+            // but also other ones like ClassDefNotFoundError etc.
+        }
+        if ( clazz.getSuperclass() != null ) {
+            // try super class
+            return searchMethod(clazz.getSuperclass(), mName, params);
+        }
+        return null;
+    }
+
+    /**
+     * Invoke the method on the object with the arguments.
+     * @return The result of the method invocation or <code>null</code> if an exception occurs.
+     */
+    public static Object invoke(final Object obj, final Method m, final Object[] args) {
+        try {
+            return m.invoke(obj, args);
+        } catch (final Throwable e) {
+            // ignore
+        }
+        return null;
+    }
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/DefaultWebConsolePlugin.java b/status-printer/src/main/java/org/apache/felix/status/impl/DefaultWebConsolePlugin.java
new file mode 100644
index 0000000..d13b2f5
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/DefaultWebConsolePlugin.java
@@ -0,0 +1,132 @@
+/*
+ * 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.status.impl;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.StatusPrinterHandler;
+import org.apache.felix.status.StatusPrinterManager;
+import org.apache.felix.status.impl.webconsole.ConsoleConstants;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * The web console plugin for a status printer.
+ */
+public class DefaultWebConsolePlugin extends AbstractWebConsolePlugin implements StatusPrinterHandler {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructor
+     * @param statusPrinterAdapter The adapter
+     */
+    DefaultWebConsolePlugin(final StatusPrinterManager statusPrinterManager) {
+        super(statusPrinterManager);
+    }
+
+    @Override
+    protected StatusPrinterHandler getStatusPrinterHandler() {
+        return this;
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getTitle()
+     */
+    public String getTitle() {
+        return "Overview";
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getName()
+     */
+    public String getName() {
+        return "config";
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getCategory()
+     */
+    public String getCategory() {
+        return "Status";
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getModes()
+     */
+    public PrinterMode[] getModes() {
+        return new PrinterMode[] {PrinterMode.TEXT};
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#supports(org.apache.felix.status.PrinterMode)
+     */
+    public boolean supports(final PrinterMode mode) {
+        return mode == PrinterMode.TEXT;
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinter#print(org.apache.felix.status.PrinterMode, java.io.PrintWriter)
+     */
+    public void print(final PrinterMode mode, final PrintWriter printWriter) {
+        final StatusPrinterHandler[] handlers = this.statusPrinterManager.getAllHandlers();
+        printWriter.print("Currently registered ");
+        printWriter.print(String.valueOf(handlers.length));
+        printWriter.println(" status printer.");
+        printWriter.println();
+        for(final StatusPrinterHandler handler : handlers) {
+            printWriter.println(handler.getTitle());
+        }
+    }
+
+    /**
+     * @see org.apache.felix.status.ZipAttachmentProvider#addAttachments(java.lang.String, java.util.zip.ZipOutputStream)
+     */
+    public void addAttachments(String namePrefix, ZipOutputStream zos)
+    throws IOException {
+        // no attachments support
+    }
+
+    public static ServiceRegistration register(final BundleContext context,
+            final StatusPrinterManager manager) {
+        final DefaultWebConsolePlugin dwcp = new DefaultWebConsolePlugin(manager);
+
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(ConsoleConstants.PLUGIN_LABEL, dwcp.getName());
+        props.put(ConsoleConstants.PLUGIN_TITLE, dwcp.getTitle());
+        props.put(ConsoleConstants.PLUGIN_CATEGORY, dwcp.getCategory());
+        return context.registerService(ConsoleConstants.INTERFACE_SERVLET, new ServiceFactory() {
+
+            public void ungetService(final Bundle bundle, final ServiceRegistration registration,
+                    final Object service) {
+                // nothing to do
+            }
+
+            public Object getService(final Bundle bundle, final ServiceRegistration registration) {
+                return dwcp;
+            }
+
+        }, props);
+    }
+}
\ No newline at end of file
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterAdapter.java b/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterAdapter.java
new file mode 100644
index 0000000..6de7079
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterAdapter.java
@@ -0,0 +1,214 @@
+/*
+ * 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.status.impl;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.text.DateFormat;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.StatusPrinter;
+import org.apache.felix.status.StatusPrinterHandler;
+import org.apache.felix.status.StatusPrinterManager;
+import org.apache.felix.status.ZipAttachmentProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * Helper class for a status printer.
+ */
+public class StatusPrinterAdapter implements StatusPrinterHandler, Comparable<StatusPrinterAdapter> {
+
+    /**
+     * Formatter pattern to render the current time of status generation.
+     */
+    static final DateFormat DISPLAY_DATE_FORMAT = DateFormat.getDateTimeInstance( DateFormat.LONG,
+        DateFormat.LONG, Locale.US );
+
+    /**
+     * Create a new adapter if the provided service is either a printer or provides
+     * the print method.
+     * @return An adapter or <code>null</code> if the method is missing.
+     */
+    public static StatusPrinterAdapter createAdapter(final StatusPrinterDescription description,
+            final Object service) {
+
+        Method printMethod = null;
+        if ( !(service instanceof StatusPrinter) ) {
+
+            // print(String, PrintWriter)
+            printMethod = ClassUtils.searchMethod(service.getClass(), "print",
+                    new Class[] {String.class, PrintWriter.class});
+            if ( printMethod == null ) {
+                return null;
+            }
+        }
+        Method attachmentMethod = null;
+        if ( !(service instanceof ZipAttachmentProvider) ) {
+
+            // addAttachments()
+            attachmentMethod = ClassUtils.searchMethod(service.getClass(), "addAttachments",
+                    new Class[] {String.class, ZipOutputStream.class});
+        }
+        return new StatusPrinterAdapter(
+                description,
+                service,
+                printMethod,
+                attachmentMethod);
+    }
+
+    /**
+     * Comparator for adapters based on the service ranking.
+     */
+    public static final Comparator<StatusPrinterAdapter> RANKING_COMPARATOR = new Comparator<StatusPrinterAdapter>() {
+
+        public int compare(final StatusPrinterAdapter o1, final StatusPrinterAdapter o2) {
+            return o1.description.compareTo(o2.description);
+        }
+    };
+
+    /** The status printer service. */
+    private final Object printer;
+
+    /** The printer description. */
+    private final StatusPrinterDescription description;
+
+    /** The method to use if printer does not implement the service interface. */
+    private final Method printMethod;
+
+    private final Method attachmentMethod;
+
+    /** Service registration for the web console. */
+    private ServiceRegistration registration;
+
+    /**
+     * Constructor.
+     */
+    public StatusPrinterAdapter( final StatusPrinterDescription description,
+            final Object printer,
+            final Method printMethod,
+            final Method attachmentMethod) {
+        this.description = description;
+        this.printer = printer;
+        this.printMethod = printMethod;
+        this.attachmentMethod = attachmentMethod;
+    }
+
+    public void registerConsole(final BundleContext context, final StatusPrinterManager manager) {
+        if ( this.registration == null &&
+             (supports(PrinterMode.HTML_BODY) || supports(PrinterMode.TEXT))) {
+            this.registration = WebConsolePlugin.register(context, manager, this.description);
+        }
+    }
+
+    public void unregisterConsole() {
+        if ( this.registration != null ) {
+            this.registration.unregister();
+            this.registration = null;
+        }
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getTitle()
+     */
+    public String getTitle() {
+        return this.description.getTitle();
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getName()
+     */
+    public String getName() {
+        return this.description.getName();
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getCategory()
+     */
+    public String getCategory() {
+        return this.description.getCategory();
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#getModes()
+     */
+    public PrinterMode[] getModes() {
+        return this.description.getModes();
+    }
+
+    /**
+     * @see org.apache.felix.status.ZipAttachmentProvider#addAttachments(java.lang.String, java.util.zip.ZipOutputStream)
+     */
+    public void addAttachments(final String namePrefix, final ZipOutputStream zos)
+    throws IOException {
+        // check if printer implements ZipAttachmentProvider
+        if ( printer instanceof ZipAttachmentProvider ) {
+            ((ZipAttachmentProvider)printer).addAttachments(namePrefix, zos);
+        } else if ( this.attachmentMethod != null ) {
+            ClassUtils.invoke(this.printer, this.attachmentMethod, new Object[] {namePrefix, zos});
+        }
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterHandler#supports(org.apache.felix.status.PrinterMode)
+     */
+    public boolean supports(final PrinterMode mode) {
+        for(int i=0; i<this.description.getModes().length; i++) {
+            if ( this.description.getModes()[i] == mode ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinter#print(org.apache.felix.status.PrinterMode, java.io.PrintWriter)
+     */
+    public void print(final PrinterMode mode,
+            final PrintWriter printWriter) {
+        if ( this.supports(mode) ) {
+            if ( this.printer instanceof StatusPrinter ) {
+                ((StatusPrinter)this.printer).print(mode, printWriter);
+            } else {
+                ClassUtils.invoke(this.printer, this.printMethod, new Object[] {mode.toString(), printWriter});
+            }
+        }
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return printer.getClass() + "(" + super.toString() + ")";
+    }
+
+    /**
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(final StatusPrinterAdapter spa) {
+        return this.description.getSortKey().compareTo(spa.description.getSortKey());
+    }
+
+    public StatusPrinterDescription getDescription() {
+        return this.description;
+    }
+ }
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterDescription.java b/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterDescription.java
new file mode 100644
index 0000000..6224ef6
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterDescription.java
@@ -0,0 +1,134 @@
+/*
+ * 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.status.impl;
+
+import java.util.Arrays;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.StatusPrinter;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Helper class for a configuration printer.
+ */
+public class StatusPrinterDescription implements Comparable<StatusPrinterDescription> {
+
+    private final ServiceReference reference;
+
+    private final PrinterMode[] modes;
+
+    private final String name;
+
+    private final String title;
+
+    private final String sortKey;
+
+    private final String category;
+
+    public StatusPrinterDescription(final ServiceReference ref) {
+        this.reference = ref;
+
+        // check modes
+        final Object modesCfg = ref.getProperty(StatusPrinter.CONFIG_PRINTER_MODES);
+        if ( modesCfg instanceof String ) {
+            this.modes = new PrinterMode[] { PrinterMode.valueOf((String)modesCfg)};
+        } else if ( modesCfg instanceof String[] ) {
+            final String[] modesCfgArray = (String[])modesCfg;
+            this.modes = new PrinterMode[modesCfgArray.length];
+            for(int i=0; i<modesCfgArray.length;i++) {
+                this.modes[i] = PrinterMode.valueOf(modesCfgArray[i]);
+            }
+        } else {
+            this.modes = null;
+        }
+
+        // check name
+        if ( ref.getProperty(StatusPrinter.CONFIG_NAME) != null ) {
+            this.name = ref.getProperty(StatusPrinter.CONFIG_NAME).toString();
+        } else {
+            this.name = null;
+        }
+
+        // check title
+        if ( ref.getProperty(StatusPrinter.CONFIG_TITLE) != null ) {
+            this.title = ref.getProperty(StatusPrinter.CONFIG_TITLE).toString();
+            if ( this.title.startsWith("%") ) {
+                this.sortKey = this.title.substring(1);
+            } else {
+                this.sortKey = this.title;
+            }
+        } else {
+            this.title = null;
+            this.sortKey = null;
+        }
+
+        // check category
+        if ( ref.getProperty(StatusPrinter.CONFIG_CATEGORY) != null ) {
+            this.category = ref.getProperty(StatusPrinter.CONFIG_CATEGORY).toString();
+        } else {
+            this.category = null;
+        }
+    }
+
+    public String getTitle() {
+        return this.title;
+    }
+
+    public String getSortKey() {
+        return this.sortKey;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getCategory() {
+        return this.category;
+    }
+
+    public PrinterMode[] getModes() {
+        return this.modes;
+    }
+
+    public ServiceReference getServiceReference() {
+        return this.reference;
+    }
+
+    /**
+     * @see java.lang.Comparable#compareTo(java.lang.Object)
+     */
+    public int compareTo(final StatusPrinterDescription spa) {
+        return this.reference.compareTo(spa.reference);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return this.reference.equals(obj);
+    }
+
+    @Override
+    public int hashCode() {
+        return this.reference.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "StatusPrinterDescription [title=" + title + ", name=" + name
+                + ", modes=" + Arrays.toString(modes) + ", sortKey=" + sortKey
+                + ", category=" + category + "]";
+    }
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterManagerImpl.java b/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterManagerImpl.java
new file mode 100644
index 0000000..09e518e
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/StatusPrinterManagerImpl.java
@@ -0,0 +1,263 @@
+/*
+ * 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.status.impl;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.StatusPrinter;
+import org.apache.felix.status.StatusPrinterHandler;
+import org.apache.felix.status.StatusPrinterManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * The manager keeps track of all status printers and maintains them
+ * based on their name. If more than one printer with the same name
+ * is registered, the one with highest service ranking is used.
+ */
+public class StatusPrinterManagerImpl implements StatusPrinterManager,
+    ServiceTrackerCustomizer {
+
+    /** Logger. */
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Bundle Context .*/
+    private final BundleContext bundleContext;
+
+    /** Service tracker for status printers. */
+    private final ServiceTracker cfgPrinterTracker;
+
+    /** All adapters mapped by their name. */
+    private final Map<String, List<StatusPrinterAdapter>> allAdapters = new HashMap<String, List<StatusPrinterAdapter>>();
+
+    /** Used adapters. */
+    private final Set<StatusPrinterAdapter> usedAdapters = new ConcurrentSkipListSet<StatusPrinterAdapter>();
+
+    /** Registration for the web console. */
+    private final ServiceRegistration pluginRegistration;
+
+    /**
+     * Create the status printer
+     * @param btx Bundle Context
+     * @throws InvalidSyntaxException Should only happen if we have an error in the code
+     */
+    public StatusPrinterManagerImpl(final BundleContext btx) throws InvalidSyntaxException {
+        this.bundleContext = btx;
+        this.cfgPrinterTracker = new ServiceTracker( this.bundleContext,
+                this.bundleContext.createFilter("(&(" + StatusPrinter.CONFIG_PRINTER_MODES + "=*)"
+                        + "(" + StatusPrinter.CONFIG_NAME + "=*)"
+                        + "(" + StatusPrinter.CONFIG_TITLE + "=*))"),
+                this );
+        this.cfgPrinterTracker.open();
+
+        this.pluginRegistration = DefaultWebConsolePlugin.register(btx, this);
+    }
+
+    /**
+     * Dispose this service
+     */
+    public void dispose() {
+        if ( this.pluginRegistration != null ) {
+            this.pluginRegistration.unregister();
+        }
+        this.cfgPrinterTracker.close();
+        synchronized ( this.allAdapters ) {
+            this.allAdapters.clear();
+        }
+        this.usedAdapters.clear();
+    }
+
+    /**
+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)
+     */
+    public Object addingService(final ServiceReference reference) {
+        final Object obj = this.bundleContext.getService(reference);
+        if ( obj != null ) {
+            this.addService(reference, obj);
+        }
+        return obj;
+    }
+
+    /**
+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService(org.osgi.framework.ServiceReference, java.lang.Object)
+     */
+    public void modifiedService(final ServiceReference reference, final Object service) {
+        this.removeService(reference);
+        this.addService(reference, service);
+    }
+
+    private void addService(final ServiceReference reference, final Object obj) {
+        final StatusPrinterDescription desc = new StatusPrinterDescription(reference);
+
+        boolean valid = true;
+        if ( desc.getModes() == null ) {
+            logger.info("Ignoring status printer - printer modes configuration is missing: {}", reference);
+            valid = false;
+        }
+        if ( desc.getName() == null ) {
+            logger.info("Ignoring status printer - name configuration is missing: {}", reference);
+            valid = false;
+        }
+        if ( desc.getTitle() == null ) {
+            logger.info("Ignoring status printer - title configuration is missing: {}", reference);
+            valid = false;
+        }
+        if ( valid ) {
+            final StatusPrinterAdapter adapter = StatusPrinterAdapter.createAdapter(desc, obj);
+            if ( adapter == null ) {
+                logger.info("Ignoring status printer - printer method is missing: {}", reference);
+            } else {
+                this.addAdapter(adapter);
+            }
+        }
+    }
+
+    private void addAdapter(final StatusPrinterAdapter adapter) {
+        StatusPrinterAdapter removeAdapter = null;
+        StatusPrinterAdapter addAdapter = null;
+
+        final String key = adapter.getName();
+        synchronized ( this.allAdapters ) {
+            List<StatusPrinterAdapter> list = this.allAdapters.get(key);
+            final StatusPrinterAdapter first;
+            if ( list == null ) {
+                list = new LinkedList<StatusPrinterAdapter>();
+                this.allAdapters.put(key, list);
+                first = null;
+            } else {
+                first = list.get(0);
+            }
+            list.add(adapter);
+            Collections.sort(list, StatusPrinterAdapter.RANKING_COMPARATOR);
+            if ( first != null ) {
+                if ( first != list.get(0) ) {
+                    // update
+                    removeAdapter = first;
+                    addAdapter = adapter;
+                }
+            } else {
+                // add
+                addAdapter = adapter;
+            }
+        }
+        if ( removeAdapter != null ) {
+            final Iterator<StatusPrinterAdapter> i = this.usedAdapters.iterator();
+            while ( i.hasNext() ) {
+                if ( i.next() == removeAdapter ) {
+                    i.remove();
+                    break;
+                }
+            }
+            removeAdapter.unregisterConsole();
+        }
+        if ( addAdapter != null ) {
+            this.usedAdapters.add(addAdapter);
+            addAdapter.registerConsole(this.bundleContext, this);
+        }
+    }
+
+    /**
+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference, java.lang.Object)
+     */
+    public void removedService(final ServiceReference reference, final Object service) {
+        this.removeService(reference);
+        this.bundleContext.ungetService(reference);
+    }
+
+    private void removeService(final ServiceReference reference) {
+        synchronized ( this.allAdapters ) {
+            final Iterator<Map.Entry<String, List<StatusPrinterAdapter>>> i = this.allAdapters.entrySet().iterator();
+            while ( i.hasNext() ) {
+                final Map.Entry<String, List<StatusPrinterAdapter>> entry = i.next();
+                final Iterator<StatusPrinterAdapter> iter = entry.getValue().iterator();
+                boolean removed = false;
+                while ( iter.hasNext() ) {
+                    final StatusPrinterAdapter adapter = iter.next();
+                    if ( adapter.getDescription().getServiceReference().compareTo(reference) == 0 ) {
+                        iter.remove();
+                        removed = true;
+                        break;
+                    }
+                }
+                if ( removed ) {
+                    if ( entry.getValue().size() == 0 ) {
+                        i.remove();
+                    }
+                    break;
+                }
+            }
+        }
+        final Iterator<StatusPrinterAdapter> iter = this.usedAdapters.iterator();
+        while ( iter.hasNext() ) {
+            final StatusPrinterAdapter adapter = iter.next();
+            if ( adapter.getDescription().getServiceReference().compareTo(reference) == 0 ) {
+                iter.remove();
+                adapter.unregisterConsole();
+                break;
+            }
+        }
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterManager#getAllHandlers()
+     */
+    public StatusPrinterHandler[] getAllHandlers() {
+        return this.usedAdapters.toArray(new StatusPrinterHandler[this.usedAdapters.size()]);
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterManager#getHandlers(org.apache.felix.status.PrinterMode)
+     */
+    public StatusPrinterHandler[] getHandlers(final PrinterMode mode) {
+        final List<StatusPrinterHandler> result = new ArrayList<StatusPrinterHandler>();
+        for(final StatusPrinterAdapter printer : this.usedAdapters) {
+            if ( printer.supports(mode) ) {
+                result.add(printer);
+            }
+        }
+        return result.toArray(new StatusPrinterHandler[result.size()]);
+    }
+
+    /**
+     * @see org.apache.felix.status.StatusPrinterManager#getHandler(java.lang.String)
+     */
+    public StatusPrinterHandler getHandler(final String name) {
+        for(final StatusPrinterAdapter printer : this.usedAdapters) {
+            if ( name.equals(printer.getName()) ) {
+                return printer;
+            }
+        }
+        return null;
+    }
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/WebConsolePlugin.java b/status-printer/src/main/java/org/apache/felix/status/impl/WebConsolePlugin.java
new file mode 100644
index 0000000..4af1d19
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/WebConsolePlugin.java
@@ -0,0 +1,78 @@
+/*
+ * 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.status.impl;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.apache.felix.status.StatusPrinterHandler;
+import org.apache.felix.status.StatusPrinterManager;
+import org.apache.felix.status.impl.webconsole.ConsoleConstants;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * The web console plugin for a status printer.
+ */
+public class WebConsolePlugin extends AbstractWebConsolePlugin {
+
+    private static final long serialVersionUID = 1L;
+
+    /** Printer name. */
+    private final String printerName;
+
+    /**
+     * Constructor
+     * @param statusPrinterManager The status printer manager.
+     * @param printerName The name of the printer this plugin is displaying.
+     */
+    WebConsolePlugin(final StatusPrinterManager statusPrinterManager,
+            final String printerName) {
+        super(statusPrinterManager);
+        this.printerName = printerName;
+    }
+
+    @Override
+    protected StatusPrinterHandler getStatusPrinterHandler() {
+        return this.statusPrinterManager.getHandler(this.printerName);
+    }
+
+    public static ServiceRegistration register(
+            final BundleContext context,
+            final StatusPrinterManager manager,
+            final StatusPrinterDescription desc) {
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(ConsoleConstants.PLUGIN_LABEL, "status-" + desc.getName());
+        props.put(ConsoleConstants.PLUGIN_TITLE, desc.getTitle());
+        props.put(ConsoleConstants.PLUGIN_CATEGORY, desc.getCategory() == null ? "Status" : desc.getCategory());
+        return context.registerService(ConsoleConstants.INTERFACE_SERVLET, new ServiceFactory() {
+
+            public void ungetService(final Bundle bundle, final ServiceRegistration registration,
+                    final Object service) {
+                // nothing to do
+            }
+
+            public Object getService(final Bundle bundle, final ServiceRegistration registration) {
+                return new WebConsolePlugin(manager, desc.getName());
+            }
+
+        }, props);
+
+    }
+}
\ No newline at end of file
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ConfigurationPrinterAdapter.java b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ConfigurationPrinterAdapter.java
new file mode 100644
index 0000000..54e3262
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ConfigurationPrinterAdapter.java
@@ -0,0 +1,234 @@
+/*
+ * 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.status.impl.webconsole;
+
+import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.impl.ClassUtils;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Helper class for a configuration printer.
+ */
+public class ConfigurationPrinterAdapter {
+
+    private final Object printer;
+    public String title;
+    public String label;
+    private final String[] modes;
+    private final boolean escapeHtml;
+    private final Method printMethod;
+    private final Method attachmentMethod;
+
+    private static final List<String> CUSTOM_MODES = new ArrayList<String>();
+    static {
+        CUSTOM_MODES.add( ConsoleConstants.MODE_TXT);
+        CUSTOM_MODES.add( ConsoleConstants.MODE_WEB );
+        CUSTOM_MODES.add( ConsoleConstants.MODE_ZIP );
+    }
+
+    /**
+     * Check whether the class implements the configuration printer.
+     * This is done manually to avoid having the configuration printer class available.
+     */
+    private static boolean isConfigurationPrinter(final Class<?> clazz) {
+        for(final Class<?> i : clazz.getInterfaces() ) {
+            if ( i.getName().equals(ConsoleConstants.INTERFACE_CONFIGURATION_PRINTER) ) {
+                return true;
+            }
+        }
+        if ( clazz.getSuperclass() != null ) {
+            return isConfigurationPrinter(clazz.getSuperclass());
+        }
+        return false;
+    }
+
+    /**
+     * Try to create a new configuration printer adapter.
+     */
+    public static ConfigurationPrinterAdapter createAdapter(
+            final Object service,
+            final ServiceReference ref) {
+        String title;
+        Object modes = null;
+        if ( isConfigurationPrinter(service.getClass()) ) {
+            modes = ref.getProperty(ConsoleConstants.CONFIG_PRINTER_MODES);
+            if ( modes == null ) {
+                modes = ref.getProperty( ConsoleConstants.PROPERTY_MODES );
+            }
+            final Method titleMethod = ClassUtils.searchMethod(service.getClass(), "getTitle", null);
+            if ( titleMethod == null ) {
+                return null;
+            }
+            title = (String)ClassUtils.invoke(service, titleMethod, null);
+        } else {
+            modes = ref.getProperty( ConsoleConstants.CONFIG_PRINTER_MODES );
+            title = (String)ref.getProperty(  ConsoleConstants.PLUGIN_TITLE );
+        }
+
+        Object cfgPrinter = null;
+        Method printMethod = null;
+
+        // first: printConfiguration(PrintWriter, String)
+        final Method method2Params = ClassUtils.searchMethod(service.getClass(), "printConfiguration",
+                new Class[] {PrintWriter.class, String.class});
+        if ( method2Params != null ) {
+            cfgPrinter = service;
+            printMethod = method2Params;
+        }
+
+        if ( cfgPrinter == null ) {
+            // second: printConfiguration(PrintWriter)
+            final Method method1Params = ClassUtils.searchMethod(service.getClass(), "printConfiguration",
+                    new Class[] {PrintWriter.class});
+            if ( method1Params != null ) {
+                cfgPrinter = service;
+                printMethod = method1Params;
+            }
+        }
+
+        if ( cfgPrinter != null ) {
+            final Object label =  ref.getProperty( ConsoleConstants.PLUGIN_LABEL );
+            // check escaping
+            boolean webUnescaped;
+            Object ehObj = ref.getProperty( ConsoleConstants.CONFIG_PRINTER_WEB_UNESCAPED );
+            if ( ehObj instanceof Boolean ) {
+                webUnescaped = ( ( Boolean ) ehObj ).booleanValue();
+            } else if ( ehObj instanceof String ) {
+                webUnescaped = Boolean.valueOf( ( String ) ehObj ).booleanValue();
+            }  else {
+                webUnescaped = false;
+            }
+
+            final String[] modesArray;
+            // check modes
+            if ( modes == null || !( modes instanceof String || modes instanceof String[] ) ) {
+                modesArray = null;
+            } else {
+                if ( modes instanceof String ) {
+                    if ( CUSTOM_MODES.contains(modes) ) {
+                        modesArray = new String[] {modes.toString()};
+                    } else {
+                        modesArray = null;
+                    }
+                } else {
+                    final String[] values = (String[])modes;
+                    boolean valid = values.length > 0;
+                    for(int i=0; i<values.length; i++) {
+                        if ( !CUSTOM_MODES.contains(values[i]) ) {
+                            valid = false;
+                            break;
+                        }
+                    }
+                    if ( valid) {
+                        modesArray = values;
+                    } else {
+                        modesArray = null;
+                    }
+                }
+            }
+
+            return new ConfigurationPrinterAdapter(
+                    cfgPrinter,
+                    printMethod,
+                    ClassUtils.searchMethod(cfgPrinter.getClass(), "getAttachments", new Class[] {String.class}),
+                    title,
+                    (label instanceof String ? (String)label : null),
+                    modesArray,
+                    !webUnescaped);
+        }
+        return null;
+    }
+
+    private ConfigurationPrinterAdapter( final Object printer,
+            final Method printMethod,
+            final Method attachmentMethod,
+            final String title,
+            final String label,
+            final String[] modesArray,
+            final boolean escapeHtml ) {
+        this.printer = printer;
+        this.title = title;
+        this.label = label;
+        this.escapeHtml = escapeHtml;
+        this.printMethod = printMethod;
+        this.attachmentMethod = attachmentMethod;
+        this.modes = modesArray;
+    }
+
+    /**
+     * Map the modes to status printer modes
+     */
+    public String[] getPrinterModes() {
+        final Set<String> list = new HashSet<String>();
+        if ( this.match(ConsoleConstants.MODE_TXT) || this.match(ConsoleConstants.MODE_ZIP) ) {
+            list.add(PrinterMode.ZIP_FILE.name());
+        }
+        if ( this.match(ConsoleConstants.MODE_WEB) ) {
+            if ( !escapeHtml ) {
+                list.add(PrinterMode.HTML_BODY.name());
+            } else {
+                list.add(PrinterMode.TEXT.name());
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    private boolean match(final String mode) {
+        if ( this.modes == null) {
+            return true;
+        }
+        for(int i=0; i<this.modes.length; i++) {
+            if ( this.modes[i].equals(mode) )  {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public final void printConfiguration( final PrintWriter pw, final String mode ) {
+        if ( printMethod.getParameterTypes().length > 1 ) {
+            ClassUtils.invoke(this.printer, this.printMethod, new Object[] {pw, mode});
+        } else {
+            ClassUtils.invoke(this.printer, this.printMethod, new Object[] {pw});
+        }
+    }
+
+    public URL[] getAttachments() {
+        // check if printer implements binary configuration printer
+        URL[] attachments = null;
+        if ( attachmentMethod != null ) {
+            attachments = (URL[])ClassUtils.invoke(printer, attachmentMethod, new Object[] {ConsoleConstants.MODE_ZIP});
+        }
+        return attachments;
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return title + " (" + printer.getClass() + ")";
+    }
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ConsoleConstants.java b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ConsoleConstants.java
new file mode 100644
index 0000000..55eb6f6
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ConsoleConstants.java
@@ -0,0 +1,45 @@
+/*
+ * 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.status.impl.webconsole;
+
+
+public class ConsoleConstants {
+
+    public static final String INTERFACE_SERVLET = "javax.servlet.Servlet"; //$NON-NLS-1$
+
+    public static final String INTERFACE_CONFIGURATION_PRINTER = "org.apache.felix.webconsole.ConfigurationPrinter"; //$NON-NLS-1$
+
+    public static final String PLUGIN_LABEL = "felix.webconsole.label"; //$NON-NLS-1$
+
+    public static final String PLUGIN_TITLE = "felix.webconsole.title"; //$NON-NLS-1$
+
+    public static final String PLUGIN_CATEGORY = "felix.webconsole.category"; //$NON-NLS-1$
+
+    public static final String CONFIG_PRINTER_MODES = "felix.webconsole.configprinter.modes"; //$NON-NLS-1$
+
+    public static final String CONFIG_PRINTER_WEB_UNESCAPED = "felix.webconsole.configprinter.web.unescaped"; //$NON-NLS-1$
+
+    public static final String MODE_ALWAYS = "always"; //$NON-NLS-1$
+
+    public static final String MODE_WEB = "web"; //$NON-NLS-1$
+
+    public static final String MODE_ZIP = "zip"; //$NON-NLS-1$
+
+    public static final String MODE_TXT = "txt"; //$NON-NLS-1$
+
+    public static final String PROPERTY_MODES = "modes"; //$NON-NLS-1$
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ResourceBundleManager.java b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ResourceBundleManager.java
new file mode 100644
index 0000000..b79323e
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/ResourceBundleManager.java
@@ -0,0 +1,179 @@
+/*
+ * 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.status.impl.webconsole;
+
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.Constants;
+
+
+/**
+ * The ResourceBundleManager manages resource bundle instance per OSGi Bundle.
+ * It contains a local cache, for bundles, but when a bundle is being unistalled,
+ * its resources stored in the cache are cleaned up.
+ */
+public class ResourceBundleManager implements BundleListener
+{
+
+    private final BundleContext bundleContext;
+
+    private final Map<Long, ResourceBundle> resourceBundleCaches;
+
+
+    /**
+     * Creates a new object and adds self as a bundle listener
+     *
+     * @param bundleContext the bundle context of the Web Console.
+     */
+    public ResourceBundleManager( final BundleContext bundleContext )
+    {
+        this.bundleContext = bundleContext;
+        this.resourceBundleCaches = new HashMap<Long, ResourceBundle>();
+
+        bundleContext.addBundleListener( this );
+    }
+
+
+    /**
+     * Removes the bundle lister.
+     */
+    public void dispose()
+    {
+        bundleContext.removeBundleListener( this );
+    }
+
+
+    /**
+     * This method is used to retrieve a /cached/ instance of the i18n resource associated
+     * with a given bundle.
+     *
+     * @param provider the bundle, provider of the resources
+     * @param locale the requested locale.
+     */
+    public ResourceBundle getResourceBundle( final Bundle provider ) {
+        ResourceBundle cache;
+        final Long key = new Long( provider.getBundleId() );
+        synchronized ( resourceBundleCaches ) {
+            cache = resourceBundleCaches.get( key );
+            if ( cache == null && !resourceBundleCaches.containsKey(key)) {
+                cache = this.loadResourceBundle(provider);
+                resourceBundleCaches.put( key, cache );
+            }
+        }
+
+        return cache;
+    }
+
+
+    // ---------- BundleListener
+
+    /**
+     * @see org.osgi.framework.BundleListener#bundleChanged(org.osgi.framework.BundleEvent)
+     */
+    public final void bundleChanged( BundleEvent event )
+    {
+        if ( event.getType() == BundleEvent.STOPPED )
+        {
+            final Long key = new Long( event.getBundle().getBundleId() );
+            synchronized ( resourceBundleCaches )
+            {
+                resourceBundleCaches.remove( key );
+            }
+        }
+    }
+
+    private static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
+
+    private ResourceBundle loadResourceBundle(final Bundle bundle) {
+        final String path = "_" + DEFAULT_LOCALE.toString(); //$NON-NLS-1$
+        final URL source = ( URL ) getResourceBundleEntries(bundle).get( path );
+        if ( source != null ) {
+            try {
+                return new PropertyResourceBundle( source.openStream() );
+            } catch ( final IOException ignore ) {
+                // ignore
+            }
+        }
+        return null;
+    }
+
+    // TODO : Instead of getting all property files, we could just get the one for the default locale
+    private synchronized Map getResourceBundleEntries(final Bundle bundle)
+    {
+        String file = ( String ) bundle.getHeaders().get( Constants.BUNDLE_LOCALIZATION );
+        if ( file == null )
+        {
+            file = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME;
+        }
+
+        // remove leading slash
+        if ( file.startsWith( "/" ) ) //$NON-NLS-1$
+        {
+            file = file.substring( 1 );
+        }
+
+        // split path and base name
+        int slash = file.lastIndexOf( '/' );
+        String fileName = file.substring( slash + 1 );
+        String path = ( slash <= 0 ) ? "/" : file.substring( 0, slash ); //$NON-NLS-1$
+
+        HashMap resourceBundleEntries = new HashMap();
+
+        Enumeration locales = bundle.findEntries( path, fileName + "*.properties", false ); //$NON-NLS-1$
+        if ( locales != null )
+        {
+            while ( locales.hasMoreElements() )
+            {
+                URL entry = ( URL ) locales.nextElement();
+
+                // calculate the key
+                String entryPath = entry.getPath();
+                final int start = entryPath.lastIndexOf( '/' ) + 1 + fileName.length(); // path, slash and base name
+                final int end = entryPath.length() - 11; // .properties suffix
+                entryPath = entryPath.substring( start, end );
+
+                // the default language is "name.properties" thus the entry
+                // path is empty and must default to "_"+DEFAULT_LOCALE
+                if (entryPath.length() == 0) {
+                    entryPath = "_" + DEFAULT_LOCALE; //$NON-NLS-1$
+                }
+
+                // only add this entry, if the "language" is not provided
+                // by the main bundle or an earlier bound fragment
+                if (!resourceBundleEntries.containsKey( entryPath )) {
+                    resourceBundleEntries.put( entryPath, entry );
+                }
+            }
+        }
+
+        return resourceBundleEntries;
+    }
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/WebConsoleAdapter.java b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/WebConsoleAdapter.java
new file mode 100644
index 0000000..ddab251
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/impl/webconsole/WebConsoleAdapter.java
@@ -0,0 +1,205 @@
+/*
+ * 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.status.impl.webconsole;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.felix.status.PrinterMode;
+import org.apache.felix.status.StatusPrinter;
+import org.apache.felix.status.ZipAttachmentProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+
+/**
+ */
+public class WebConsoleAdapter implements ServiceTrackerCustomizer {
+
+    private final BundleContext bundleContext;
+
+    private final ServiceTracker cfgPrinterTracker;
+
+    private final Map<ServiceReference, ServiceRegistration> registrations = new HashMap<ServiceReference, ServiceRegistration>();
+
+    private final ResourceBundleManager rbManager;
+
+    public WebConsoleAdapter(final BundleContext btx) throws InvalidSyntaxException {
+        this.bundleContext = btx;
+        this.rbManager = new ResourceBundleManager(btx);
+        this.cfgPrinterTracker = new ServiceTracker( this.bundleContext,
+                this.bundleContext.createFilter("(|(" + Constants.OBJECTCLASS + "=" + ConsoleConstants.INTERFACE_CONFIGURATION_PRINTER + ")" +
+                        "(&(" + ConsoleConstants.PLUGIN_LABEL + "=*)(&("
+                        + ConsoleConstants.PLUGIN_TITLE + "=*)("
+                        + ConsoleConstants.CONFIG_PRINTER_MODES + "=*))))"),
+                this );
+        this.cfgPrinterTracker.open();
+    }
+
+    /**
+     * Dispose this service
+     */
+    public void dispose() {
+        this.cfgPrinterTracker.close();
+        synchronized ( this.registrations ) {
+            for(final ServiceRegistration reg : this.registrations.values()) {
+                reg.unregister();
+            }
+            this.registrations.clear();
+        }
+        this.rbManager.dispose();
+    }
+
+    public void add(final ServiceReference reference, final Object service) {
+        final ConfigurationPrinterAdapter cpa = ConfigurationPrinterAdapter.createAdapter(service, reference);
+        if ( cpa != null && cpa.title != null ) {
+            if ( cpa.title.startsWith("%") ) {
+                final String key = cpa.title.substring(1);
+                final ResourceBundle rb = this.rbManager.getResourceBundle(reference.getBundle());
+                if ( rb == null || !rb.containsKey(key) ) {
+                    cpa.title = key;
+                } else {
+                    cpa.title = rb.getString(key);
+                }
+            }
+            if ( cpa.label == null ) {
+                cpa.label = cpa.title;
+            }
+            final Dictionary<String, Object> props = new Hashtable<String, Object>();
+            props.put(StatusPrinter.CONFIG_NAME, cpa.label);
+            props.put(StatusPrinter.CONFIG_TITLE, cpa.title);
+            props.put(StatusPrinter.CONFIG_PRINTER_MODES, cpa.getPrinterModes());
+
+            if ( reference.getProperty(ConsoleConstants.PLUGIN_CATEGORY) != null ) {
+                props.put(StatusPrinter.CONFIG_CATEGORY, reference.getProperty(ConsoleConstants.PLUGIN_CATEGORY));
+            }
+            final ServiceRegistration reg = this.bundleContext.registerService(StatusPrinter.class.getName(), new ZipAttachmentProvider() {
+
+                /**
+                 * @see org.apache.felix.status.StatusPrinter#print(org.apache.felix.status.PrinterMode, java.io.PrintWriter)
+                 */
+                public void print(final PrinterMode mode, final PrintWriter printWriter) {
+                    final String m;
+                    if ( mode == PrinterMode.HTML_BODY ) {
+                        m = ConsoleConstants.MODE_WEB;
+                    } else if ( mode == PrinterMode.TEXT ) {
+                        m = ConsoleConstants.MODE_TXT;
+                    } else if ( mode == PrinterMode.ZIP_FILE ) {
+                        m = ConsoleConstants.MODE_ZIP;
+                    } else {
+                        m = null;
+                    }
+                    if ( m != null ) {
+                        cpa.printConfiguration(printWriter, m);
+                    }
+                }
+
+                /**
+                 * @see org.apache.felix.status.ZipAttachmentProvider#addAttachments(java.lang.String, java.util.zip.ZipOutputStream)
+                 */
+                public void addAttachments(final String namePrefix, final ZipOutputStream zos)
+                throws IOException {
+                    final URL[] attachments = cpa.getAttachments();
+                    if ( attachments != null ) {
+                        for(final URL current : attachments) {
+                            final String path = current.getPath();
+                            final String name;
+                            if ( path == null || path.length() == 0 ) {
+                                // sanity code, we should have a path, but if not let's
+                                // just create some random name
+                                name = "file" + Double.doubleToLongBits( Math.random() );
+                            } else {
+                                final int pos = path.lastIndexOf('/');
+                                name = (pos == -1 ? path : path.substring(pos + 1));
+                            }
+                            final ZipEntry entry = new ZipEntry(namePrefix + name);
+                            zos.putNextEntry(entry);
+                            final InputStream is = current.openStream();
+                            try {
+                                byte[] buffer = new byte[4096];
+                                int n = 0;
+                                while (-1 != (n = is.read(buffer))) {
+                                    zos.write(buffer, 0, n);
+                                }
+                            } finally {
+                                if ( is != null ) {
+                                    try { is.close(); } catch (final IOException ignore) {}
+                                }
+                            }
+                            zos.closeEntry();
+                        }
+                    }
+                }
+
+            }, props);
+            synchronized ( this.registrations ) {
+                this.registrations.put(reference, reg);
+            }
+        }
+    }
+
+    private final void remove(final ServiceReference reference) {
+        final ServiceRegistration reg;
+        synchronized ( this.registrations ) {
+            reg = this.registrations.remove(reference);
+        }
+        if ( reg != null ) {
+            reg.unregister();
+        }
+    }
+
+    /**
+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#addingService(org.osgi.framework.ServiceReference)
+     */
+    public Object addingService(final ServiceReference reference) {
+        final Object service = this.bundleContext.getService(reference);
+        if ( service != null ) {
+            this.add(reference, service);
+        }
+        return service;
+    }
+    /**
+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#modifiedService(org.osgi.framework.ServiceReference, java.lang.Object)
+     */
+    public void modifiedService(final ServiceReference reference, final Object service) {
+        this.remove(reference);
+        this.add(reference, service);
+    }
+
+    /**
+     * @see org.osgi.util.tracker.ServiceTrackerCustomizer#removedService(org.osgi.framework.ServiceReference, java.lang.Object)
+     */
+    public void removedService(final ServiceReference reference, final Object service) {
+        this.remove(reference);
+        this.bundleContext.ungetService(reference);
+    }
+}
diff --git a/status-printer/src/main/java/org/apache/felix/status/package-info.java b/status-printer/src/main/java/org/apache/felix/status/package-info.java
new file mode 100644
index 0000000..8d647e8
--- /dev/null
+++ b/status-printer/src/main/java/org/apache/felix/status/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+/**
+ * Interfaces for getting status information about the current instance
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+@Export(optional = "provide:=true")
+package org.apache.felix.status;
+
+import aQute.bnd.annotation.Export;
+import aQute.bnd.annotation.Version;
+
+