FELIX-2224 Add Configuration Admin support for the Memory Usage plugin

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@926544 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole-plugins/memoryusage/pom.xml b/webconsole-plugins/memoryusage/pom.xml
index d7b5e5f..1aeafcf 100644
--- a/webconsole-plugins/memoryusage/pom.xml
+++ b/webconsole-plugins/memoryusage/pom.xml
@@ -69,9 +69,17 @@
                             javax.management.*,
                             org.osgi.framework,
                             org.slf4j.*,
-                            javax.servlet.*;
+                            
+                            <!-- plug into the traditional Felix shell -->
                             org.apache.felix.shell;
-                            org.apache.felix.webconsole;resolution:=optional
+                            
+                            <!-- plug into the web console -->
+                            javax.servlet.*;
+                            org.apache.felix.webconsole;
+                            
+                            <!-- support configuration -->
+                            org.osgi.service.cm;
+                            org.osgi.service.metatype;resolution:=optional
                         </Import-Package>
                         <Private-Package>
                             org.apache.felix.webconsole.plugins.memoryusage.*
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/Activator.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/Activator.java
index e964971..18d0fc1 100644
--- a/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/Activator.java
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/Activator.java
@@ -62,6 +62,20 @@
         {
             // web console might not be available, don't care
         }
+
+        // register for configuration
+        try
+        {
+            MemoryUsageConfigurator tdp = new MemoryUsageConfigurator(support);
+            Dictionary<String, Object> properties = new Hashtable<String, Object>();
+            properties.put(Constants.SERVICE_PID, MemoryUsageConfigurator.NAME);
+            register(bundleContext, new String[]
+                { "org.osgi.service.cm.ManagedService" }, tdp, properties);
+        }
+        catch (Throwable t)
+        {
+            // Configuration Admin and Metatype Service API might not be available, don't care
+        }
     }
 
     public void stop(BundleContext bundleContext)
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageConfigurator.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageConfigurator.java
new file mode 100644
index 0000000..8aad994
--- /dev/null
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageConfigurator.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.webconsole.plugins.memoryusage.internal;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.metatype.AttributeDefinition;
+import org.osgi.service.metatype.MetaTypeProvider;
+import org.osgi.service.metatype.ObjectClassDefinition;
+
+public class MemoryUsageConfigurator implements ManagedService, MetaTypeProvider
+{
+
+    static final String NAME = "org.apache.felix.webconsole.plugins.memoryusage.internal.MemoryUsageConfigurator";
+
+    private static final String PROP_DUMP_THRESHOLD = "dump.threshold";
+
+    private static final String PROP_DUMP_LOCATION = "dump.location";
+
+    private final MemoryUsageSupport support;
+
+    private ObjectClassDefinition ocd;
+
+    MemoryUsageConfigurator(final MemoryUsageSupport support)
+    {
+        this.support = support;
+    }
+
+    @SuppressWarnings("unchecked")
+    public void updated(Dictionary properties) throws ConfigurationException
+    {
+        final Object thresholdValue = properties.get(PROP_DUMP_THRESHOLD);
+        if (thresholdValue != null)
+        {
+            final int threshold;
+            if (thresholdValue instanceof Number)
+            {
+                threshold = ((Number) thresholdValue).intValue();
+            }
+            else
+            {
+                // try to convert
+                try
+                {
+                    threshold = Integer.parseInt(thresholdValue.toString());
+                }
+                catch (NumberFormatException nfe)
+                {
+                    throw failure(thresholdValue);
+                }
+            }
+
+            try
+            {
+                support.setThreshold(threshold);
+            }
+            catch (IllegalArgumentException iae)
+            {
+                throw failure(iae.getMessage());
+            }
+        }
+
+        final Object locationValue = properties.get(PROP_DUMP_LOCATION);
+        if (locationValue instanceof String)
+        {
+            support.setDumpLocation((String) locationValue);
+        }
+    }
+
+    public String[] getLocales()
+    {
+        return null;
+    }
+
+    public ObjectClassDefinition getObjectClassDefinition(String id, String locale)
+    {
+        if (!NAME.equals(id))
+        {
+            return null;
+        }
+
+        if (ocd == null)
+        {
+
+            final ArrayList<AttributeDefinition> adList = new ArrayList<AttributeDefinition>();
+            adList.add(new AttributeDefinitionImpl(PROP_DUMP_THRESHOLD, "Dump Threshold",
+                "Threshold at which to automatically create a memory dump as a percentage in the range "
+                    + MemoryUsageSupport.MIN_DUMP_THRESHOLD + " to " + MemoryUsageSupport.MAX_DUMP_THRESHOLD
+                    + " or zero to disable automatic dump creation.", AttributeDefinition.INTEGER, new String[]
+                    { String.valueOf(MemoryUsageSupport.DEFAULT_DUMP_THRESHOLD) }, 0, null, null)
+            {
+                @Override
+                public String validate(String value)
+                {
+                    try
+                    {
+                        int threshold = Integer.parseInt(value);
+                        if (threshold != 0
+                            && (threshold < MemoryUsageSupport.MIN_DUMP_THRESHOLD || threshold > MemoryUsageSupport.MAX_DUMP_THRESHOLD))
+                        {
+                            return "Threshold must in the range " + MemoryUsageSupport.MIN_DUMP_THRESHOLD + " to "
+                                + MemoryUsageSupport.MAX_DUMP_THRESHOLD + " or zero";
+                        }
+                        return ""; // everything ok
+                    }
+                    catch (NumberFormatException nfe)
+                    {
+                        return "Threshhold must be numeric";
+                    }
+                }
+            });
+            adList.add(new AttributeDefinitionImpl(PROP_DUMP_LOCATION, "Dumpe Location",
+                "The filesystem location where heap dumps are stored. If this is null or empty (the default) the dumps are stored in "
+                    + support.getDefaultDumpLocation(), ""));
+
+            ocd = new ObjectClassDefinition()
+            {
+
+                private final AttributeDefinition[] attrs = adList.toArray(new AttributeDefinition[adList.size()]);
+
+                public String getName()
+                {
+                    return "Apache Felix Web Console Memory Usage Plugin";
+                }
+
+                public InputStream getIcon(int arg0)
+                {
+                    return null;
+                }
+
+                public String getID()
+                {
+                    return NAME;
+                }
+
+                public String getDescription()
+                {
+                    return "Configuration of the Apache Felix Web Console Memory Usage Plugin.";
+                }
+
+                public AttributeDefinition[] getAttributeDefinitions(int filter)
+                {
+                    return (filter == OPTIONAL) ? null : attrs;
+                }
+            };
+        }
+
+        return ocd;
+    }
+
+    private ConfigurationException failure(final Object invalidValue)
+    {
+        return new ConfigurationException(PROP_DUMP_THRESHOLD, "Invalid Dump Threshold value '" + invalidValue
+            + "': Must be an integer number in the range " + MemoryUsageSupport.MIN_DUMP_THRESHOLD + " to "
+            + MemoryUsageSupport.MAX_DUMP_THRESHOLD + " or zero to disable");
+    }
+
+    private static class AttributeDefinitionImpl implements AttributeDefinition
+    {
+
+        private final String id;
+        private final String name;
+        private final String description;
+        private final int type;
+        private final String[] defaultValues;
+        private final int cardinality;
+        private final String[] optionLabels;
+        private final String[] optionValues;
+
+        AttributeDefinitionImpl(final String id, final String name, final String description, final String defaultValue)
+        {
+            this(id, name, description, STRING, new String[]
+                { defaultValue }, 0, null, null);
+        }
+
+        AttributeDefinitionImpl(final String id, final String name, final String description, final int type,
+            final String[] defaultValues, final int cardinality, final String[] optionLabels,
+            final String[] optionValues)
+        {
+            this.id = id;
+            this.name = name;
+            this.description = description;
+            this.type = type;
+            this.defaultValues = defaultValues;
+            this.cardinality = cardinality;
+            this.optionLabels = optionLabels;
+            this.optionValues = optionValues;
+        }
+
+        public int getCardinality()
+        {
+            return cardinality;
+        }
+
+        public String[] getDefaultValue()
+        {
+            return defaultValues;
+        }
+
+        public String getDescription()
+        {
+            return description;
+        }
+
+        public String getID()
+        {
+            return id;
+        }
+
+        public String getName()
+        {
+            return name;
+        }
+
+        public String[] getOptionLabels()
+        {
+            return optionLabels;
+        }
+
+        public String[] getOptionValues()
+        {
+            return optionValues;
+        }
+
+        public int getType()
+        {
+            return type;
+        }
+
+        public String validate(String arg0)
+        {
+            return null;
+        }
+    }
+}
diff --git a/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageSupport.java b/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageSupport.java
index 78a92e2..be79755 100644
--- a/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageSupport.java
+++ b/webconsole-plugins/memoryusage/src/main/java/org/apache/felix/webconsole/plugins/memoryusage/internal/MemoryUsageSupport.java
@@ -1,18 +1,18 @@
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
+ * or more contributor license agreements. See the NOTICE file
  * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
+ * 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
+ * with the License. You may obtain a copy of the License at
  *
- *   http://www.apache.org/licenses/LICENSE-2.0
+ * 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
+ * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
  */
@@ -46,28 +46,56 @@
 final class MemoryUsageSupport implements NotificationListener
 {
 
+    /**
+     * The minimum allowed automatic heap dump threshold (value is 50).
+     */
+    static final int MIN_DUMP_THRESHOLD = 50;
+
+    /**
+     * The maximum allowed automatic heap dump threshold (value is 99).
+     */
+    static final int MAX_DUMP_THRESHOLD = 99;
+
+    /**
+     * The default automatic heap dump threshold if none has been configured
+     * (or no configuration has yet been provided) (value is 95).
+     */
+    static final int DEFAULT_DUMP_THRESHOLD = 95;
+
     // This is the name of the HotSpot Diagnostic MBean
     private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic";
 
     private final Logger log = LoggerFactory.getLogger(getClass());
 
-    private final File dumpLocation;
+    // the default dump location: the dumps folder in the bundle private data
+    // or the current working directory
+    private final File defaultDumpLocation;
 
+    // the configured dump location
+    private File dumpLocation;
+
+    // the actual threshold (configured or dynamically set in the console UI)
     private int threshold;
 
     MemoryUsageSupport(final BundleContext context)
     {
+        // get the dump location
         File dumps = context.getDataFile("dumps");
         if (dumps == null)
         {
             dumps = new File("dumps");
         }
+        defaultDumpLocation = dumps.getAbsoluteFile();
 
-        dumpLocation = dumps;
-        dumpLocation.mkdirs();
+        // prepare the dump location
+        setDumpLocation(null);
 
+        // register for memory threshold notifications
         NotificationEmitter memEmitter = (NotificationEmitter) getMemory();
         memEmitter.addNotificationListener(this, null, null);
+
+        // set the initial automatic dump threshold
+        setThreshold(DEFAULT_DUMP_THRESHOLD);
     }
 
     void dispose()
@@ -77,19 +105,26 @@
         {
             memEmitter.removeNotificationListener(this);
         }
-        catch (ListenerNotFoundException lnfes)
+        catch (ListenerNotFoundException e)
         {
-            // TODO: not expected really ?
+            // don't care
         }
     }
 
+    /**
+     * Sets the threshold percentage.
+     *
+     * @param percentage The threshold as a percentage of memory consumption.
+     *            This value may be 0 (zero) to switch off automatic heap dumps
+     *            or in the range {@link #MIN_DUMP_THRESHOLD} to
+     *            {@link #MAX_DUMP_THRESHOLD}.
+     * @throws IllegalArgumentException if the percentage value is outside of
+     *             the valid range of thresholds. The message is the percentage
+     *             value which is not accepted.
+     */
     final void setThreshold(final int percentage)
     {
-        if (percentage < 50 || percentage > 100)
-        {
-            // wrong value
-        }
-        else
+        if (threshold == 0 || (threshold >= MIN_DUMP_THRESHOLD && threshold <= MAX_DUMP_THRESHOLD))
         {
             List<MemoryPoolMXBean> pools = getMemoryPools();
             for (MemoryPoolMXBean pool : pools)
@@ -102,6 +137,10 @@
             }
             this.threshold = percentage;
         }
+        else
+        {
+            throw new IllegalArgumentException(String.valueOf(threshold));
+        }
     }
 
     final int getThreshold()
@@ -189,10 +228,10 @@
             buf.append(",'type':'").append(pool.getType()).append('\'');
 
             MemoryUsage usage = pool.getUsage();
-            usedTotal += doNumber(buf, "used", usage.getUsed());
-            initTotal += doNumber(buf, "init", usage.getInit());
-            committedTotal += doNumber(buf, "committed", usage.getCommitted());
-            maxTotal += doNumber(buf, "max", usage.getMax());
+            usedTotal += formatNumber(buf, "used", usage.getUsed());
+            initTotal += formatNumber(buf, "init", usage.getInit());
+            committedTotal += formatNumber(buf, "committed", usage.getCommitted());
+            maxTotal += formatNumber(buf, "max", usage.getMax());
 
             final long score = 100L * usage.getUsed() / usage.getMax();
             buf.append(",'score':'").append(score).append("%'");
@@ -203,10 +242,10 @@
         // totalisation
         buf.append("{");
         buf.append("'name':'Total','type':'TOTAL'");
-        doNumber(buf, "used", usedTotal);
-        doNumber(buf, "init", initTotal);
-        doNumber(buf, "committed", committedTotal);
-        doNumber(buf, "max", maxTotal);
+        formatNumber(buf, "used", usedTotal);
+        formatNumber(buf, "init", initTotal);
+        formatNumber(buf, "committed", committedTotal);
+        formatNumber(buf, "max", maxTotal);
 
         final long score = 100L * usedTotal / maxTotal;
         buf.append(",'score':'").append(score).append("%'");
@@ -217,7 +256,7 @@
         return buf.toString();
     }
 
-    private long doNumber(final StringBuilder buf, final String title, final long value)
+    long formatNumber(final StringBuilder buf, final String title, final long value)
     {
 
         final BigDecimal KB = new BigDecimal(1000L);
@@ -250,6 +289,28 @@
         return value;
     }
 
+    final String getDefaultDumpLocation()
+    {
+        return defaultDumpLocation.getAbsolutePath();
+    }
+
+    final void setDumpLocation(final String dumpLocation)
+    {
+        if (dumpLocation == null || dumpLocation.length() == 0)
+        {
+            this.dumpLocation = defaultDumpLocation;
+        }
+        else
+        {
+            this.dumpLocation = new File(dumpLocation).getAbsoluteFile();
+        }
+    }
+
+    final File getDumpLocation()
+    {
+        return dumpLocation;
+    }
+
     final void listDumpFiles(final PrintHelper pw)
     {
         pw.title(dumpLocation.getAbsolutePath(), 1);
@@ -317,10 +378,14 @@
      *
      * @param live <code>true</code> if only live objects are to be returned
      * @return
-     * @throws NoSuchElementException If no provided mechanism is successfully used to create a heap dump
+     * @throws NoSuchElementException If no provided mechanism is successfully
+     *             used to create a heap dump
      */
     final File dumpHeap(String name, final boolean live)
     {
+        // ensure dumplocation exists
+        dumpLocation.mkdirs();
+
         File dump = dumpSunMBean(name, live);
         if (dump == null)
         {
@@ -372,10 +437,11 @@
         void keyVal(final String key, final Object value);
     }
 
-    //---------- Various System Specific Heap Dump mechanisms
+    // ---------- Various System Specific Heap Dump mechanisms
 
     /**
-     * @see http://blogs.sun.com/sundararajan/entry/programmatically_dumping_heap_from_java
+     * @see http://blogs.sun.com/sundararajan/entry/
+     *      programmatically_dumping_heap_from_java
      */
     private File dumpSunMBean(String name, boolean live)
     {
@@ -384,7 +450,6 @@
             name = "heap." + System.currentTimeMillis() + ".hprof";
         }
 
-        dumpLocation.mkdirs();
         File tmpFile = new File(dumpLocation, name);
         MBeanServer server = ManagementFactory.getPlatformMBeanServer();
 
@@ -409,7 +474,9 @@
     /**
      * @param name
      * @return
-     * @see http://publib.boulder.ibm.com/infocenter/javasdk/v5r0/index.jsp?topic=/com.ibm.java.doc.diagnostics.50/diag/tools/heapdump_enable.html
+     * @see http
+     *      ://publib.boulder.ibm.com/infocenter/javasdk/v5r0/index.jsp?topic
+     *      =/com.ibm.java.doc.diagnostics.50/diag/tools/heapdump_enable.html
      */
     private File dumpIbmDump(String name)
     {