FELIX-2208 Move memoryusage web console plugin from fmeschbe's Sling whiteboard to here


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@925007 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole-plugins/memoryusage/pom.xml b/webconsole-plugins/memoryusage/pom.xml
new file mode 100644
index 0000000..aa3bbe0
--- /dev/null
+++ b/webconsole-plugins/memoryusage/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+    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.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>8</version>
+    </parent>
+
+    <artifactId>org.apache.sling.extensions.memoryusage</artifactId>
+    <packaging>bundle</packaging>
+    <version>0.1.0-SNAPSHOT</version>
+
+    <name>Apache Sling Memory Usage Tracker</name>
+    <description>
+        Plugin providing plugins to the Felix Shell and Web Console to
+        observe the VMs memory use and cause try to get thread dumps etc.
+    </description>
+
+    <scm>
+        <connection>
+            scm:svn:http://svn.apache.org/repos/asf/sling/whiteboard/fmeschbe/memoryusage
+        </connection>
+        <developerConnection>
+            scm:svn:https://svn.apache.org/repos/asf/sling/whiteboard/fmeschbe/memoryusage
+        </developerConnection>
+        <url>
+            http://svn.apache.org/viewvc/sling/whiteboard/fmeschbe/memoryusage
+        </url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            javax.management.*,
+                            org.osgi.framework,
+                            org.slf4j.*,
+                            javax.servlet.*;
+                            org.apache.felix.shell;
+                            org.apache.felix.webconsole;resolution:=optional
+                        </Import-Package>
+                        <Private-Package>
+                            org.apache.sling.extensions.memoryusage.*
+                        </Private-Package>
+                        <Bundle-Activator>
+                            org.apache.sling.extensions.memoryusage.internal.Activator
+                        </Bundle-Activator>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.shell</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.webconsole</artifactId>
+            <version>2.0.7-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/Activator.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/Activator.java
new file mode 100644
index 0000000..6346cb0
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/Activator.java
@@ -0,0 +1,84 @@
+/*
+ * 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.sling.extensions.memoryusage.internal;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+
+public class Activator implements BundleActivator {
+
+    private MemoryUsageSupport support;
+
+    public void start(BundleContext bundleContext) {
+
+        support = new MemoryUsageSupport(bundleContext);
+
+        // install thread handler shell command
+        try {
+            register(bundleContext,
+                new String[] { "org.apache.felix.shell.Command" },
+                new MemoryUsageCommand(support), null);
+        } catch (Throwable t) {
+            // shell service might not be available, don't care
+        }
+
+        // install Web Console plugin
+        try {
+            MemoryUsagePanel tdp = new MemoryUsagePanel(support);
+            tdp.activate(bundleContext);
+
+            Dictionary<String, Object> properties = new Hashtable<String, Object>();
+            properties.put("felix.webconsole.label", tdp.getLabel());
+
+            register(bundleContext, new String[] { "javax.servlet.Servlet",
+                "org.apache.felix.webconsole.ConfigurationPrinter" }, tdp,
+                properties);
+        } catch (Throwable t) {
+            // web console might not be available, don't care
+        }
+    }
+
+    public void stop(BundleContext bundleContext) {
+        if (support != null) {
+            support.dispose();
+            support = null;
+        }
+    }
+
+    private void register(BundleContext context, String[] serviceNames,
+            Object service, Dictionary<String, Object> properties) {
+
+        // ensure properties
+        if (properties == null) {
+            properties = new Hashtable<String, Object>();
+        }
+
+        // default settings
+        properties.put(Constants.SERVICE_DESCRIPTION, "Memory Usage ("
+            + serviceNames[0] + ")");
+        properties.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
+
+        context.registerService(serviceNames, service, properties);
+    }
+
+}
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsageCommand.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsageCommand.java
new file mode 100644
index 0000000..f65d66e
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsageCommand.java
@@ -0,0 +1,232 @@
+/*
+ * 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.sling.extensions.memoryusage.internal;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.StringTokenizer;
+
+import org.apache.felix.shell.Command;
+
+public class MemoryUsageCommand implements Command {
+
+    private static final String HELP_CMD = "help";
+
+    private static final String POOLS_CMD = "pools";
+
+    private static final String DUMP_CMD = "dump";
+
+    private static final String OPT_DUMP_ALL = "-a";
+
+    private static final String LS_CMD = "ls";
+
+    private static final String RM_CMD = "rm";
+
+    private final MemoryUsageSupport support;
+
+    public MemoryUsageCommand(final MemoryUsageSupport support) {
+        this.support = support;
+    }
+
+    public void execute(String commandLine, final PrintStream out,
+            final PrintStream err) {
+
+        final PrintWriterPrintHelper printHelper = new PrintWriterPrintHelper(
+            out);
+
+        // Parse the commandLine to get the mem command.
+        final StringTokenizer st = new StringTokenizer(commandLine);
+        // Ignore the invoking command.
+        st.nextToken();
+        // Try to get the mem command, default is simple memory info dump.
+        String command = null;
+        try {
+            command = st.nextToken();
+        } catch (Exception ex) {
+            // Ignore.
+        }
+
+        // Perform the specified command.
+        if (command == null) {
+            mem(printHelper);
+        } else if (command.equals(HELP_CMD)) {
+            help(out, st);
+        } else if (command.equals(POOLS_CMD)) {
+            pools(printHelper);
+        } else if (command.equals(DUMP_CMD)) {
+            dump(out, st, err);
+        } else if (command.equals(LS_CMD)) {
+            ls(printHelper);
+        } else if (command.equals(RM_CMD)) {
+            rm(out, err, st);
+        } else {
+            err.println("Unknown command: " + command);
+        }
+
+        printHelper.flush();
+    }
+
+    public String getName() {
+        return "mem";
+    }
+
+    public String getShortDescription() {
+        return "Prints Java VM Memory Consumption or writes a heap dump";
+    }
+
+    public String getUsage() {
+        return "mem help";
+    }
+
+    // ---------- internal
+
+    private void mem(final MemoryUsageSupport.PrintHelper printHelper) {
+        support.printOverallMemory(printHelper);
+    }
+
+    private void pools(final MemoryUsageSupport.PrintHelper printHelper) {
+        support.printMemoryPools(printHelper);
+    }
+
+    private void help(final PrintStream out, final StringTokenizer word) {
+        String command = HELP_CMD;
+        if (word.hasMoreTokens()) {
+            command = word.nextToken();
+        }
+        if (command.equals(DUMP_CMD)) {
+            out.printf("%s %s [ %s ]%n", getName(), DUMP_CMD, OPT_DUMP_ALL);
+            out.println("This command requests a heap dump to be created. If the\n"
+                + "-a option is not added, only live objects are dumped.");
+        } else if (command.equals(LS_CMD)) {
+            out.printf("%s %s%n", getName(), LS_CMD);
+            out.println("This command lists all heap dumps created with the\n"
+                + DUMP_CMD + " command.");
+        } else if (command.equals(RM_CMD)) {
+            out.printf("%s %s <dump>%n", getName(), RM_CMD);
+            out.println("This command removes the indicated heap dump file.\n"
+                + "Use the " + LS_CMD
+                + " to list heap dumps which may be removed.");
+        } else if (command.equals(POOLS_CMD)) {
+            out.printf("%s %s%n", getName(), POOLS_CMD);
+            out.println("This command shows information about all memory pools.");
+        } else {
+            out.printf("%s %s [ %s | %s | %s | %s ]%n", getName(), HELP_CMD,
+                DUMP_CMD, LS_CMD, RM_CMD, POOLS_CMD);
+            out.printf("%s %s [ %s ]%n", getName(), DUMP_CMD, OPT_DUMP_ALL);
+            out.printf("%s %s%n", getName(), LS_CMD);
+            out.printf("%s %s <dump>%n", getName(), RM_CMD);
+            out.printf("%s %s%n", getName(), POOLS_CMD);
+            out.println("Using the "
+                + getName()
+                + " command without any arguments prints a memory use overview.");
+        }
+    }
+
+    private void dump(final PrintStream out, final StringTokenizer words,
+            final PrintStream err) {
+        String dumpTarget = null;
+        boolean all = false;
+
+        while (words.hasMoreTokens()) {
+            String word = words.nextToken();
+            if (OPT_DUMP_ALL.equals(word)) {
+                all = true;
+            } else {
+                dumpTarget = word;
+            }
+        }
+
+        try {
+            File dumpFile = support.dumpHeap(dumpTarget, !all);
+            out.println("Heap dumped to " + dumpFile + " (" + dumpFile.length()
+                + " bytes)");
+        } catch (Exception e) {
+            err.println("Problem dumping heap");
+            e.printStackTrace(err);
+        }
+    }
+
+    private void ls(final MemoryUsageSupport.PrintHelper printHelper) {
+        support.listDumpFiles(printHelper);
+    }
+
+    private void rm(final PrintStream out, final PrintStream err,
+            final StringTokenizer words) {
+        if (words.hasMoreTokens()) {
+            do {
+                final String name = words.nextToken();
+                if (support.rmDumpFile(name)) {
+                    out.println("rm: " + name + " removed");
+                    out.flush();
+                } else {
+                    err.println("rm: " + name + ": No such dump file");
+                    err.flush();
+                }
+            } while (words.hasMoreTokens());
+        }
+    }
+
+    private static class PrintWriterPrintHelper implements
+            MemoryUsageSupport.PrintHelper {
+
+        private static final String INDENTS = "          ";
+
+        private final PrintStream pw;
+
+        private String indent;
+
+        PrintWriterPrintHelper(final PrintStream pw) {
+            this.pw = pw;
+            this.indent = "";
+        }
+
+        public void title(String title, int level) {
+            pw.printf("%s%s%n", getIndent(level - 1), title);
+            indent = getIndent(level);
+        }
+
+        public void val(String value) {
+            pw.printf("%s%s%n", indent, value);
+        }
+
+        public void keyVal(final String key, final Object value) {
+            if (value == null) {
+                val(key);
+            } else {
+                pw.printf("%s%s: %s%n", indent, key, value);
+            }
+        }
+
+        void flush() {
+            pw.flush();
+        }
+
+        private static String getIndent(final int level) {
+            if (level <= 0) {
+                return "";
+            }
+
+            final int indent = 2 * level;
+            if (indent > INDENTS.length()) {
+                return INDENTS;
+            }
+            return INDENTS.substring(0, indent);
+        }
+    }
+}
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsagePanel.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsagePanel.java
new file mode 100644
index 0000000..60481ab
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsagePanel.java
@@ -0,0 +1,379 @@
+/*
+ * 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.sling.extensions.memoryusage.internal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.AttachmentProvider;
+import org.apache.felix.webconsole.ConfigurationPrinter;
+import org.apache.felix.webconsole.DefaultVariableResolver;
+import org.apache.felix.webconsole.WebConsoleUtil;
+
+@SuppressWarnings("serial")
+public class MemoryUsagePanel extends AbstractWebConsolePlugin implements
+        ConfigurationPrinter, AttachmentProvider {
+
+    private final MemoryUsageSupport support;
+
+    public MemoryUsagePanel(final MemoryUsageSupport support) {
+        this.support = support;
+    }
+
+    // ---------- AbstractWebConsolePlugin
+
+    @Override
+    public String getLabel() {
+        return "memoryusage";
+    }
+
+    @Override
+    public String getTitle() {
+        return "Heap Dumps";
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void renderContent(HttpServletRequest req, HttpServletResponse res)
+            throws IOException {
+        final PrintWriter pw = res.getWriter();
+
+        final StringBuilder statusBuf = new StringBuilder(1024);
+        statusBuf.append('[');
+
+        final StringBuilder filesBuf = new StringBuilder(1024);
+        filesBuf.append('[');
+
+        final File[] files = support.getDumpFiles();
+        if (files != null) {
+            long totalSize = 0;
+
+            for (File file : files) {
+                filesBuf.append('{');
+                filesBuf.append("'name':'").append(file.getName()).append("',");
+                filesBuf.append("'date':").append(file.lastModified()).append(
+                    ",");
+                filesBuf.append("'size':").append(file.length());
+                filesBuf.append("},");
+
+                totalSize += file.length();
+            }
+
+            statusBuf.append(files.length);
+            statusBuf.append(',');
+            statusBuf.append(totalSize);
+
+        } else {
+            statusBuf.append("0,0");
+        }
+
+        filesBuf.append(']');
+        statusBuf.append(']');
+
+        JsonPrintHelper jph = new JsonPrintHelper();
+        support.printOverallMemory(jph);
+
+        DefaultVariableResolver resolver = (DefaultVariableResolver) WebConsoleUtil.getVariableResolver(req);
+        resolver.put("__files__", filesBuf.toString());
+        resolver.put("__status__", statusBuf.toString());
+        resolver.put("__threshold__", String.valueOf(support.getThreshold()));
+        resolver.put("__overall__", jph.getString());
+
+        String template = readTemplateFile("/templates/memoryusage.html");
+        pw.println(template);
+    }
+
+    // ---------- Configuration Printer
+
+    public void printConfiguration(PrintWriter pw) {
+        support.printMemory(new PrintWriterPrintHelper(pw));
+    }
+
+    // ---------- AttachmentProvider
+
+    public URL[] getAttachments(String mode) {
+        if (ConfigurationPrinter.MODE_ZIP.equals(mode)) {
+            File[] dumpFiles = support.getDumpFiles();
+            if (dumpFiles != null && dumpFiles.length > 0) {
+                URL[] attachs = new URL[dumpFiles.length];
+                for (int i = 0; i < dumpFiles.length; i++) {
+                    try {
+                        attachs[i] = dumpFiles[i].toURI().toURL();
+                    } catch (MalformedURLException mue) {
+                        // not expected ...
+                    }
+                }
+                return attachs;
+            }
+        }
+
+        // not ZIP mode, return nothing
+        return null;
+    }
+
+    // ---------- GenericServlet
+
+    @Override
+    protected void doGet(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException, IOException {
+
+        final DumpFile dumpFile = getDumpFile(request);
+        if (dumpFile != null) {
+            spool(dumpFile.dumpFile, response, dumpFile.compress);
+        }
+
+        super.doGet(request, response);
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+
+        if ("DELETE".equals(req.getParameter("X-Request-Method"))) {
+            doDelete(req, resp);
+        } else {
+            String command = req.getParameter("command");
+            if ("dump".equals(command)) {
+                try {
+                    File file = support.dumpHeap(null, false);
+                    resp.setContentType("text/plain");
+                    resp.setCharacterEncoding("UTF-8");
+                    resp.getWriter().print("Dumped heap to " + file.getName());
+                } catch (Exception e) {
+                    // TODO: handle
+                }
+            } else if ("gc".equals(command)) {
+                System.gc();
+            } else if ("threshold".equals(command)) {
+                try {
+                    int threshold = Integer.parseInt(req.getParameter("threshold"));
+                    support.setThreshold(threshold);
+                } catch (Exception e) {
+                    // ignore
+                }
+                resp.sendRedirect(req.getRequestURI());
+            } else {
+                super.doPost(req, resp);
+            }
+        }
+    }
+
+    @Override
+    protected void doDelete(HttpServletRequest request,
+            HttpServletResponse response) throws IOException {
+        final DumpFile dumpFile = getDumpFile(request);
+        if (dumpFile != null) {
+            dumpFile.dumpFile.delete();
+            response.setStatus(HttpServletResponse.SC_OK);
+        } else {
+            response.sendError(HttpServletResponse.SC_FORBIDDEN);
+        }
+    }
+
+    // ---------- internal
+
+    private DumpFile getDumpFile(final HttpServletRequest request) {
+        final String pathInfo = request.getPathInfo();
+        if (pathInfo != null && !pathInfo.endsWith(getLabel())) {
+            final int lastSlash = pathInfo.lastIndexOf('/');
+            if (lastSlash > 0) {
+                String label = pathInfo.substring(lastSlash + 1);
+                boolean isZip = false;
+                if (label.endsWith(".zip")) {
+                    label = label.substring(0, label.length() - 4);
+                    isZip = true;
+                }
+                File dumpFile = support.getDumpFile(label);
+                if (dumpFile != null) {
+                    return new DumpFile(dumpFile, isZip);
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private void spool(final File dumpFile, final HttpServletResponse response,
+            boolean compress) throws IOException {
+        InputStream ins = null;
+        try {
+            ins = new FileInputStream(dumpFile);
+
+            response.setDateHeader("Last-Modified", dumpFile.lastModified());
+            WebConsoleUtil.setNoCache(response);
+
+            OutputStream out = response.getOutputStream();
+
+            if (compress) {
+                ZipOutputStream zip = new ZipOutputStream(out);
+                zip.setLevel(Deflater.BEST_SPEED);
+
+                ZipEntry entry = new ZipEntry(dumpFile.getName());
+                entry.setTime(dumpFile.lastModified());
+                entry.setMethod(ZipEntry.DEFLATED);
+
+                zip.putNextEntry(entry);
+
+                out = zip;
+
+                // zip output with unknown length
+                response.setContentType("application/zip");
+
+            } else {
+
+                String type = getServletContext().getMimeType(
+                    dumpFile.getName());
+                if (type == null) {
+                    type = "application/octet-stream";
+                }
+
+                response.setContentType(type);
+                response.setHeader("Content-Length",
+                    String.valueOf(dumpFile.length())); // might be bigger than
+                // int
+            }
+
+            byte[] buf = new byte[32768];
+            int rd = 0;
+            while ((rd = ins.read(buf)) >= 0) {
+                out.write(buf, 0, rd);
+            }
+
+            if (compress) {
+                out.flush();
+                ((ZipOutputStream) out).closeEntry();
+                ((ZipOutputStream) out).finish();
+            }
+
+        } finally {
+            if (ins != null) {
+                try {
+                    ins.close();
+                } catch (IOException ignore) {
+                    // ignore
+                }
+            }
+        }
+    }
+
+    private static class DumpFile {
+
+        final File dumpFile;
+
+        final boolean compress;
+
+        DumpFile(final File dumpFile, final boolean compress) {
+            this.dumpFile = dumpFile;
+            this.compress = compress;
+        }
+    }
+
+    private static class PrintWriterPrintHelper implements
+            MemoryUsageSupport.PrintHelper {
+
+        private static final String INDENTS = "          ";
+
+        private final PrintWriter pw;
+
+        private String indent;
+
+        PrintWriterPrintHelper(final PrintWriter pw) {
+            this.pw = pw;
+            this.indent = "";
+        }
+
+        public void title(String title, int level) {
+            pw.printf("%n%s%s%n", getIndent(level), title);
+            indent = getIndent(level + 1);
+        }
+
+        public void val(String value) {
+            pw.printf("%s%s%n", indent, value);
+        }
+
+        public void keyVal(final String key, final Object value) {
+            if (value == null) {
+                val(key);
+            } else {
+                pw.printf("%s%s: %s%n", indent, key, value);
+            }
+        }
+
+        private static String getIndent(final int level) {
+            final int indent = 2 * level;
+            if (indent > INDENTS.length()) {
+                return INDENTS;
+            }
+            return INDENTS.substring(0, indent);
+        }
+    }
+
+    private static class JsonPrintHelper implements
+            MemoryUsageSupport.PrintHelper {
+
+        private final StringBuilder buf;
+
+        JsonPrintHelper() {
+            buf = new StringBuilder();
+            buf.append('{');
+        }
+
+        String getString() {
+            final String result = buf.append('}').toString();
+            buf.delete(1, buf.length());
+            return result;
+        }
+
+        public void title(String title, int level) {
+        }
+
+        public void keyVal(String key, Object value) {
+            if (value == null) {
+                val(key);
+            } else {
+                buf.append('\'');
+                buf.append(key);
+                buf.append("':'");
+                buf.append(value);
+                buf.append("',");
+            }
+        }
+
+        public void val(String value) {
+            buf.append("'");
+            buf.append(value);
+            buf.append("':'',");
+        }
+
+    }
+}
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsageSupport.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsageSupport.java
new file mode 100644
index 0000000..1901472
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/sling/extensions/memoryusage/internal/MemoryUsageSupport.java
@@ -0,0 +1,258 @@
+/*
+ * 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.sling.extensions.memoryusage.internal;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryMXBean;
+import java.lang.management.MemoryNotificationInfo;
+import java.lang.management.MemoryPoolMXBean;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import javax.management.ListenerNotFoundException;
+import javax.management.MBeanServer;
+import javax.management.Notification;
+import javax.management.NotificationEmitter;
+import javax.management.NotificationListener;
+import javax.management.ObjectName;
+
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class MemoryUsageSupport implements NotificationListener {
+
+    // This is the name of the HotSpot Diagnostic MBean
+    private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic";
+
+    private final File dumpLocation;
+
+    private int threshold;
+
+    MemoryUsageSupport(final BundleContext context) {
+        final String slingHome = context.getProperty("sling.home");
+        if (slingHome != null) {
+            dumpLocation = new File(slingHome, "dumps");
+        } else {
+            dumpLocation = new File("dumps");
+        }
+        dumpLocation.mkdirs();
+
+        NotificationEmitter memEmitter = (NotificationEmitter) getMemory();
+        memEmitter.addNotificationListener(this, null, null);
+    }
+
+    void dispose() {
+        NotificationEmitter memEmitter = (NotificationEmitter) getMemory();
+        try {
+            memEmitter.removeNotificationListener(this);
+        } catch (ListenerNotFoundException lnfes) {
+            // TODO: not expected really ?
+        }
+    }
+
+    final void setThreshold(final int percentage) {
+        if (percentage < 50 || percentage > 100) {
+            // wrong value
+        } else {
+            List<MemoryPoolMXBean> pools = getMemoryPools();
+            for (MemoryPoolMXBean pool : pools) {
+                if (pool.isUsageThresholdSupported()) {
+                    long threshold = pool.getUsage().getMax() * percentage
+                        / 100;
+                    pool.setUsageThreshold(threshold);
+                }
+            }
+            this.threshold = percentage;
+        }
+    }
+
+    final int getThreshold() {
+        return threshold;
+    }
+
+    final void printMemory(final PrintHelper pw) {
+        pw.title("Overall Memory Use", 0);
+        pw.keyVal("Heap Dump Threshold", getThreshold() + "%");
+        printOverallMemory(pw);
+
+        pw.title("Memory Pools", 0);
+        printMemoryPools(pw);
+
+        pw.title("Heap Dumps", 0);
+        listDumpFiles(pw);
+    }
+
+    final void printOverallMemory(final PrintHelper pw) {
+        final MemoryMXBean mem = getMemory();
+
+        pw.keyVal("Verbose Memory Output", (mem.isVerbose() ? "yes" : "no"));
+        pw.keyVal("Pending Finalizable Objects",
+            mem.getObjectPendingFinalizationCount());
+
+        pw.keyVal("Overall Heap Memory Usage", mem.getHeapMemoryUsage());
+        pw.keyVal("Overall Non-Heap Memory Usage", mem.getNonHeapMemoryUsage());
+    }
+
+    final void printMemoryPools(final PrintHelper pw) {
+        final List<MemoryPoolMXBean> pools = getMemoryPools();
+        for (MemoryPoolMXBean pool : pools) {
+            final String title = String.format("%s (%s, %s)", pool.getName(),
+                pool.getType(), (pool.isValid() ? "valid" : "invalid"));
+            pw.title(title, 1);
+
+            pw.keyVal("Memory Managers",
+                Arrays.asList(pool.getMemoryManagerNames()));
+
+            pw.keyVal("Peak Usage", pool.getPeakUsage());
+
+            pw.keyVal("Usage", pool.getUsage());
+            if (pool.isUsageThresholdSupported()) {
+                pw.keyVal("Usage Threshold", String.format(
+                    "%d, %s, #exceeded=%d", pool.getUsageThreshold(),
+                    pool.isUsageThresholdExceeded()
+                            ? "exceeded"
+                            : "not exceeded", pool.getUsageThresholdCount()));
+            } else {
+                pw.val("Usage Threshold: not supported");
+            }
+            pw.keyVal("Collection Usage", pool.getCollectionUsage());
+            if (pool.isCollectionUsageThresholdSupported()) {
+                pw.keyVal("Collection Usage Threshold", String.format(
+                    "%d, %s, #exceeded=%d", pool.getCollectionUsageThreshold(),
+                    pool.isCollectionUsageThresholdExceeded()
+                            ? "exceeded"
+                            : "not exceeded",
+                    pool.getCollectionUsageThresholdCount()));
+            } else {
+                pw.val("Collection Usage Threshold: not supported");
+            }
+        }
+    }
+
+    final void listDumpFiles(final PrintHelper pw) {
+        pw.title(dumpLocation.getAbsolutePath(), 1);
+        File[] dumps = getDumpFiles();
+        if (dumps == null || dumps.length == 0) {
+            pw.keyVal("-- None", null);
+        } else {
+            long totalSize = 0;
+            for (File dump : dumps) {
+                // 32167397 2010-02-25 23:30 thefile
+                pw.val(String.format("%10d %tF %2$tR %s", dump.length(),
+                    new Date(dump.lastModified()), dump.getName()));
+                totalSize += dump.length();
+            }
+            pw.val(String.format("%d files, %d bytes", dumps.length, totalSize));
+        }
+    }
+
+    final File getDumpFile(final String name) {
+        // expect a non-empty string without slash
+        if (name == null || name.length() == 0 || name.indexOf('/') >= 0) {
+            return null;
+        }
+
+        File dumpFile = new File(dumpLocation, name);
+        if (dumpFile.isFile()) {
+            return dumpFile;
+        }
+
+        return null;
+    }
+
+    final File[] getDumpFiles() {
+        return dumpLocation.listFiles();
+    }
+
+    final boolean rmDumpFile(final String name) {
+        if (name == null || name.length() == 0) {
+            return false;
+        }
+
+        final File dumpFile = new File(dumpLocation, name);
+        if (!dumpFile.exists()) {
+            return false;
+        }
+
+        dumpFile.delete();
+        return true;
+    }
+
+    /**
+     * Dumps the heap to a temporary file
+     *
+     * @param live <code>true</code> if only live objects are to be returned
+     * @return
+     * @see http://blogs.sun.com/sundararajan/entry/
+     *      programmatically_dumping_heap_from_java
+     */
+    final File dumpHeap(String name, final boolean live) throws Exception {
+        try {
+            if (name == null) {
+                name = "heap." + System.currentTimeMillis() + ".bin";
+            }
+            dumpLocation.mkdirs();
+            File tmpFile = new File(dumpLocation, name);
+            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+            server.invoke(new ObjectName(HOTSPOT_BEAN_NAME), "dumpHeap",
+                new Object[] { tmpFile.getAbsolutePath(), live }, new String[] {
+                    String.class.getName(), boolean.class.getName() });
+            return tmpFile;
+            // } catch (JMException je) {
+            // } catch (IOException ioe) {
+        } finally {
+
+        }
+
+        // failure
+        // return null;
+    }
+
+    final MemoryMXBean getMemory() {
+        return ManagementFactory.getMemoryMXBean();
+    }
+
+    final List<MemoryPoolMXBean> getMemoryPools() {
+        return ManagementFactory.getMemoryPoolMXBeans();
+    }
+
+    public void handleNotification(Notification notification, Object handback) {
+        Logger log = LoggerFactory.getLogger(getClass());
+        String notifType = notification.getType();
+        if (notifType.equals(MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)) {
+            try {
+                File file = dumpHeap(null, true);
+                log.warn("Heap dumped to " + file);
+            } catch (Exception e) {
+                log.error("Failed dumping heap", e);
+            }
+        }
+    }
+
+    static interface PrintHelper {
+        void title(final String title, final int level);
+
+        void val(final String value);
+
+        void keyVal(final String key, final Object value);
+    }
+}
diff --git a/webconsole-plugins/memoryusage/src/main/resources/OSGI-INF/l10n/bundle.properties b/webconsole-plugins/memoryusage/src/main/resources/OSGI-INF/l10n/bundle.properties
new file mode 100644
index 0000000..7b8c3a9
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -0,0 +1,44 @@
+#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.
+
+#
+# Web Console strings for reference all strings here are commented.
+# This file may be used to produce a translation of the strings
+#
+# Note that properties files are ISO-8859-1 encoded. To provide translations
+# for languages requiring different character encodings, you may use the
+# native2ascii Maven Plugin from http://mojo.codehaus.org/native2ascii-maven-plugin/
+# to translate the natively encoded files to ISO-8859-1 during bundle build
+#
+# Translations requiring non-ISO-8859-1 encoding are placed in the
+# src/main/native2ascii/OSGI-INF/l10n folder and are converted using said
+# plugin while building the bundle
+#
+
+
+# Memory Usage Plugin
+dump.dump = Dump Heap
+dump.gc = Run Garbage Collector
+
+dump.overview = Memory Usage Overview
+dump.threshold = Automatic Heap Dump Threshold (%)
+dump.threshold.set = Set Threshold
+
+dump.statline = Dump Files: {0} file(s), {1} bytes
+dump.name = File Name
+dump.date = Creation Date
+dump.size = File Size
+dump.action = Action
+dump.remove = Remove Heap Dump
diff --git a/webconsole-plugins/memoryusage/src/main/resources/templates/memoryusage.html b/webconsole-plugins/memoryusage/src/main/resources/templates/memoryusage.html
new file mode 100644
index 0000000..55131d9
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/resources/templates/memoryusage.html
@@ -0,0 +1,151 @@
+<script type="text/javascript">
+// <![CDATA[
+
+function renderData(/* Object[] */ files) {
+    $("#plugin_table > tbody > tr").remove();
+    var tbody = $("#plugin_table > tbody");
+    for ( var idx in files ) {
+        var dumpFile = files[idx];
+        var trElement = tr( null, { id: "entry" + dumpFile.name } );
+        entryInternal( trElement,  dumpFile );
+        tbody.append(trElement);   
+    }
+}
+
+function entryInternal( /* Element */ parent, /* Object */ dumpFile ) {
+    var name = dumpFile.name;
+    var date = new Date(dumpFile.date).toLocaleString();
+    var size = dumpFile.size.toLocaleString();
+
+    var titleElement = createElement ("a", null, {
+        href: window.location.pathname + "/" + name
+    });
+    titleElement.appendChild(text(name));
+
+    var zipDL = createElement ("a", null, {
+        href: window.location.pathname + "/" + name + ".zip"
+    });
+    zipDL.appendChild(text("(zipped)"));
+
+    var liElement = createElement( "li", "dynhover ui-state-default ui-corner-all", {
+        title: "${dump.remove}"
+    });
+    var ulElement = createElement( "ul", "icons ui-widget", null, [
+        liElement
+    ]);
+    
+    parent.appendChild( td( null, null, [ titleElement, text(" "), zipDL ] ) );
+    parent.appendChild( td( null, null, [ text( date ) ] ) );
+    parent.appendChild( td( null, null, [ text( size ) ] ) );
+    parent.appendChild( td( null, null, [ ulElement ] ) );
+    
+    $(liElement)
+        .html('<span class="ui-icon ui-icon-trash"></span>')
+        .click(function() {
+            $.post(window.location.pathname + "/" + name,
+                { "X-Request-Method": "DELETE" },
+                function(data) {
+                    Xalert( data, "${dump.remove}" );
+                    window.location = window.location;
+                }
+            );
+        });
+}
+
+function renderOverall(/* Object */ overall) {
+    var tbody = $("#overview > tbody");
+    for ( var idx in overall ) {
+        var trElement = tr( null, null, [
+            td( null, null, [ text( idx ) ] ),
+            td( null, null, [ text( overall[idx] ) ] )
+        ]);
+        tbody.append(trElement);   
+    }
+}
+
+var __files__ = ${__files__};
+var __status__ = ${__status__};
+var __overall__ = ${__overall__};
+
+$(document).ready(function() {
+
+    $(".statline").html("${dump.statline}".msgFormat(__status__[0].toLocaleString(), __status__[1].toLocaleString()));
+
+    renderData(__files__);
+    renderOverall(__overall__);
+    
+    $("#dumpButton")
+        .click(function() {
+            $.post(window.location.pathname,
+                { "command": "dump" },
+                function(data) {
+                    Xalert( data, "${dump.dump}" );
+                    window.location = window.location;
+                }
+            );
+        });
+        
+    $("#gcButton")
+        .click(function() {
+            $.post(window.location.pathname,
+                { "command": "gc" },
+                function(data) {
+                    Xalert( data, "${dump.gc}" );
+                    window.location = window.location;
+                }
+            );
+        });
+
+    
+});
+
+
+
+// ]]>
+</script>
+
+<!-- status line -->
+<p class="statline">&nbsp;</p>
+
+<!-- top header -->
+<div class="ui-widget-header ui-corner-top buttonGroup">
+    <button type="button" id="dumpButton">${dump.dump}</button>
+    <button type="button" id="gcButton">${dump.gc}</button>
+</div>
+
+
+<table id="overview" class="tablesorter nicetable noauto">
+    <thead>
+        <tr>
+            <th colspan="2">${dump.overview}</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>${dump.threshold}</td>
+            <td>
+                <form method="post" action="">
+                    <input type="hidden" name="command" value="threshold" />
+                    <input type="text" name="threshold" value="${__threshold__}" />
+                    <input type="submit" name="submit" value="${dump.threshold.set}" />
+                </form>
+            </td>
+        </tr>
+    </tbody>
+</table>
+
+<p>&nbsp;</p>
+
+<table id="plugin_table" class="tablesorter nicetable noauto">
+    <thead>
+        <tr>
+            <th class="col_Name">${dump.name}</th>
+            <th class="col_Date">${dump.date}</th>
+            <th class="col_Size">${dump.size}</th>
+            <th class="col_Size">${dump.action}</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr><td colspan="4">&nbsp;</td></tr>
+    </tbody>
+</table>