SLING-11456 export mbeans as metrics
diff --git a/pom.xml b/pom.xml
index 546bd18..600fce6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,12 @@
       <scope>provided</scope>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.12.0</version>
+      <scope>provided</scope>
+    </dependency>
     <!-- Apache Felix -->
     <dependency>
       <groupId>org.apache.felix</groupId>
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/JmxExporterFactory.java b/src/main/java/org/apache/sling/commons/metrics/internal/JmxExporterFactory.java
new file mode 100644
index 0000000..96deabe
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/JmxExporterFactory.java
@@ -0,0 +1,178 @@
+/*
+ * 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.commons.metrics.internal;
+
+import java.lang.management.ManagementFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import javax.management.AttributeNotFoundException;
+import javax.management.InstanceNotFoundException;
+import javax.management.IntrospectionException;
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanException;
+import javax.management.MBeanInfo;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.ReflectionException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.commons.metrics.MetricsService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * This ServiceFactory allows to export JMX object names as metrics (gauge).
+ *
+ */
+
+@Component()
+@Designate(ocd=JmxExporterFactory.Config.class, factory=true)
+public class JmxExporterFactory {
+
+    
+    @ObjectClassDefinition(name="JMX to Metrics Exporter")
+    public @interface Config {
+        
+        @AttributeDefinition(name="objectnames", description="export all attribute of the MBeans matching these objectnames as Sling Metrics"
+                + "(see https://docs.oracle.com/en/java/javase/11/docs/api/java.management/javax/management/ObjectName.html")
+        String[] objectnames();
+        
+        @AttributeDefinition
+        String webconsole_configurationFactory_nameHint() default "Pattern: {objectnames}";
+    }
+    
+    
+    private static final Logger LOG = LoggerFactory.getLogger(JmxExporterFactory.class);
+    
+    @Reference
+    MetricsService metrics;
+    
+    MBeanServer server;
+    
+    @Activate
+    public void activate(Config config) {
+        server = ManagementFactory.getPlatformMBeanServer();
+        registerMetrics(config.objectnames());
+    }
+    
+
+    /**
+     * Register all applicable metrics for an objectname pattern
+     * @param pattern describes a objectname pattern
+     */
+    private void registerMetrics(String[] patterns) {
+        
+        for (String patternString : patterns) {
+            try {
+                ObjectName pattern = new ObjectName(patternString);
+                Set<ObjectName> allMBeans = server.queryNames(pattern, null);
+                if (allMBeans.isEmpty()) {
+                    LOG.info("pattern {} does not match any MBean", patternString);
+                } else {
+                    allMBeans.forEach(objectname -> {
+                        LOG.debug("registering properties for {}", objectname.toString());
+                        try {
+                            registerMBeanProperties(objectname);
+                        } catch (IntrospectionException | InstanceNotFoundException | ReflectionException e) {
+                            LOG.error("Cannot register metrics for objectname = {}", objectname.toString(),e);
+                        }
+                    });
+                }
+            } catch (MalformedObjectNameException e) {
+                LOG.error("cannot create an objectname from pattern {}",patternString,e);
+            }
+        }
+        
+    }
+    
+    
+    protected void registerMBeanProperties(ObjectName objectname) throws InstanceNotFoundException, ReflectionException, IntrospectionException {
+        MBeanInfo info = server.getMBeanInfo(objectname);
+        MBeanAttributeInfo[] attributes = info.getAttributes();
+        for (MBeanAttributeInfo attr : attributes) {
+            LOG.debug("Checking mbean = {}, name = {}, type={}",objectname, attr.getName(), attr.getType());
+            
+            Supplier<?> supplier = null;
+            if ("int".equals(attr.getType())) {
+                supplier = getSupplier(objectname, attr.getName(),0);
+            } else if ("long".equals(attr.getType())) {
+                supplier = getSupplier(objectname, attr.getName(),0L);
+            } else if ("java.lang.String".equals(attr.getType())) {
+                supplier = getSupplier(objectname,attr.getName(),"");
+            } else if ("double".equals(attr.getType())) {
+                supplier = getSupplier(objectname,attr.getName(), Double.valueOf(0.0));
+            }
+            
+            if (supplier != null) {
+                String metricName = toMetricName(objectname, attr.getName());
+                LOG.info("Registering metric {} from MBean (objectname=[{}], name={}, type={})", 
+                        metricName,objectname.toString(), attr.getName(), attr.getType());
+                metrics.gauge(metricName, supplier);
+            }
+        }
+    }
+    
+    
+    private <T> Supplier<T> getSupplier ( ObjectName name, String attributeName, T defaultValue ) {
+        
+        Supplier<T> supplier = () -> {
+            try {
+                return (T) server.getAttribute(name, attributeName);
+            } catch (InstanceNotFoundException | AttributeNotFoundException | ReflectionException
+                    | MBeanException e) {
+                LOG.warn("error when retrieving value for MBean (objectname=[{}], attribute={})",name, attributeName,e);
+                return defaultValue;
+            }
+            
+        };
+        return supplier;
+    }
+    
+    
+    protected String toMetricName(ObjectName objectName, String attributeName) {
+        String name = "sling"; // default domain
+
+        if (!StringUtils.isBlank(objectName.getDomain())) {
+            name = objectName.getDomain();
+        }
+        Hashtable<String,String> allkeys = objectName.getKeyPropertyList();
+        List<String> keyValues = new ArrayList<>(allkeys.values());
+        Collections.sort(keyValues);
+        
+        for (String s: keyValues) {
+            name += "." + s;
+        }
+        name += "." + attributeName;
+        return name;
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/commons/metrics/internal/JmxExporterFactoryTest.java b/src/test/java/org/apache/sling/commons/metrics/internal/JmxExporterFactoryTest.java
new file mode 100644
index 0000000..cfa62ba
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/metrics/internal/JmxExporterFactoryTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.commons.metrics.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.never;
+
+import java.lang.management.ManagementFactory;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import javax.management.InstanceAlreadyExistsException;
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanRegistrationException;
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.NotCompliantMBeanException;
+import javax.management.ObjectName;
+
+import org.apache.sling.commons.metrics.MetricsService;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JmxExporterFactoryTest {
+    
+    @Rule
+    public OsgiContext context = new OsgiContext();
+    
+    @Captor
+    ArgumentCaptor<Supplier<Integer>> intSupplierCaptor;
+    
+    @Captor
+    ArgumentCaptor<Supplier<Long>> longSupplierCaptor;
+    
+    @Captor
+    ArgumentCaptor<Supplier<String>> stringSupplierCaptor;
+    
+    @Captor
+    ArgumentCaptor<Supplier<Double>> doubleSupplierCaptor;
+    
+    JmxExporterFactory exporter;
+    
+    private static final String OBJECT_NAME_0 = "org.apache.sling.whiteboard.jmxexporter.impl0:type=sample1";
+    private static final String OBJECT_NAME_1 = "org.apache.sling.whiteboard.jmxexporter.impl0.impl2:type=sample2";
+    private static final String OBJECT_NAME_2 = "org.apache.sling.whiteboard.jmxexporter.impl1:type=sample3";
+    
+    // Query which will only match OBJECT_NAME_0 and OBJECT_NAME_1
+    private static final String OBJECT_NAME_QUERY = "org.apache.sling.whiteboard.jmxexporter.impl0*:type=*";
+    
+    private static final String EXPECTED_0_INT_NAME  = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.Int";
+    private static final String EXPECTED_0_LONG_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.Long";
+    private static final String EXPECTED_0_STRING_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.String";
+    private static final String EXPECTED_0_DOUBLE_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.sample1.Double";
+    
+    private static final String EXPECTED_1_INT_NAME  = "org.apache.sling.whiteboard.jmxexporter.impl0.impl2.sample2.Int";
+    private static final String EXPECTED_1_LONG_NAME = "org.apache.sling.whiteboard.jmxexporter.impl0.impl2.sample2.Long";
+    
+    private static final String EXPECTED_2_INT_NAME  = "org.apache.sling.whiteboard.jmxexporter.impl1.sample3.Int";
+    
+    private static final Double STATIC_DOUBLE = 1.0;
+    
+    MetricsService metrics;
+    
+    SimpleBean mbeans[] = { new SimpleBean(0,0L), new SimpleBean(1,1L), new SimpleBean(2,2L)};
+    
+    
+    @Before
+    public void setup() throws MalformedObjectNameException, InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
+        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+
+        server.registerMBean(mbeans[0],new ObjectName(OBJECT_NAME_0));        
+        server.registerMBean(mbeans[1],new ObjectName(OBJECT_NAME_1));
+        server.registerMBean(mbeans[2],new ObjectName(OBJECT_NAME_2));
+        
+        exporter = new JmxExporterFactory(); 
+        metrics = Mockito.mock(MetricsService.class);
+        context.registerService(MetricsService.class, metrics);
+    }
+    
+    @After
+    public void shutdown() throws MBeanRegistrationException, InstanceNotFoundException, MalformedObjectNameException {
+        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+        server.unregisterMBean(new ObjectName(OBJECT_NAME_0));
+        server.unregisterMBean(new ObjectName(OBJECT_NAME_1));
+        server.unregisterMBean(new ObjectName(OBJECT_NAME_2));
+    }
+    
+    @Test
+    public void test() {
+        Map<String,Object> props = new HashMap<>();
+        props.put("objectnames", new String[]{OBJECT_NAME_QUERY});
+        context.registerInjectActivateService(exporter, props);
+        
+        // Integer
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_INT_NAME), intSupplierCaptor.capture());
+        assertEquals(new Integer(0),intSupplierCaptor.getValue().get());
+        mbeans[0].setInt(10);
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_INT_NAME), intSupplierCaptor.capture());
+        assertEquals(new Integer(10),intSupplierCaptor.getValue().get());
+        
+        // Long
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_LONG_NAME), longSupplierCaptor.capture());
+        assertEquals(new Long(0L),longSupplierCaptor.getValue().get());
+        
+        // String
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_STRING_NAME), stringSupplierCaptor.capture());
+        assertEquals("sample",stringSupplierCaptor.getValue().get());
+        
+        // Double
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_0_DOUBLE_NAME), doubleSupplierCaptor.capture());
+        assertEquals(STATIC_DOUBLE,doubleSupplierCaptor.getValue().get());
+        
+        // MBean 1
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_1_INT_NAME), intSupplierCaptor.capture());
+        assertEquals(new Integer(1),intSupplierCaptor.getValue().get());
+        
+        Mockito.verify(metrics).gauge(Mockito.eq(EXPECTED_1_LONG_NAME), longSupplierCaptor.capture());
+        assertEquals(new Long(1L),longSupplierCaptor.getValue().get());
+        
+        // verify that no metrics for MBean2 have been registered
+        Mockito.verify(metrics, never()).gauge(Mockito.eq(EXPECTED_2_INT_NAME), intSupplierCaptor.capture());
+        
+    }
+    
+    static class SimpleBean implements SimpleBeanMBean {
+
+        
+        int internalInt = 0;
+        long internalLong = 0L;
+        
+        public SimpleBean(int i, long l) {
+            internalInt = i;
+            internalLong = l;
+        }
+        
+        @Override
+        public int getInt() {
+            return internalInt;
+        }
+        
+        @Override
+        public long getLong() {
+            return internalLong;
+        }
+        
+        public void setInt(int value) {
+            internalInt = value;
+        }
+        
+        public String getString() {
+            return "sample";
+        }
+        
+        public double getDouble() {
+            return STATIC_DOUBLE;
+        }
+        
+    }
+    
+    
+    static public interface SimpleBeanMBean {
+        
+        public int getInt();
+        public long getLong();
+        public String getString();
+        public double getDouble();
+        
+    }
+    
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java b/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java
index dd8f339..8282eb5 100644
--- a/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java
+++ b/src/test/java/org/apache/sling/commons/metrics/test/MetricsServiceFactoryIT.java
@@ -54,6 +54,7 @@
             // Commons Metrics
             testBundle("bundle.filename"),
             mavenBundle().groupId("io.dropwizard.metrics").artifactId("metrics-core").versionAsInProject(),
+            mavenBundle().groupId("org.apache.commons").artifactId("commons-lang3").versionAsInProject(),
             junitBundles()
         );
     }