diff --git a/agent/core/src/main/java/org/jolokia/backend/MBeanServerExecutorLocal.java b/agent/core/src/main/java/org/jolokia/backend/MBeanServerExecutorLocal.java index 0c23c4de2..bd64df7b9 100644 --- a/agent/core/src/main/java/org/jolokia/backend/MBeanServerExecutorLocal.java +++ b/agent/core/src/main/java/org/jolokia/backend/MBeanServerExecutorLocal.java @@ -126,8 +126,8 @@ public Object handleRequest(JsonRequestHandler pReques /** * Lifecycle method called at the end of life for this object. */ - public void destroy() { - super.destroy(); + public void unregisterFromMBeanNotifications() { + super.unregisterFromMBeanNotifications(); mbeanServers.destroy(); } diff --git a/agent/core/src/main/java/org/jolokia/backend/MBeanServerHandler.java b/agent/core/src/main/java/org/jolokia/backend/MBeanServerHandler.java index 9d93f0777..98d56d5df 100644 --- a/agent/core/src/main/java/org/jolokia/backend/MBeanServerHandler.java +++ b/agent/core/src/main/java/org/jolokia/backend/MBeanServerHandler.java @@ -8,12 +8,14 @@ import org.jolokia.backend.executor.MBeanServerExecutor; import org.jolokia.backend.executor.NotChangedException; +import org.jolokia.backend.plugin.*; import org.jolokia.config.ConfigKey; import org.jolokia.config.Configuration; import org.jolokia.detector.*; import org.jolokia.handler.JsonRequestHandler; import org.jolokia.request.JmxRequest; -import org.jolokia.util.*; +import org.jolokia.util.LogHandler; +import org.jolokia.util.ServiceObjectFactory; /* * Copyright 2009-2013 Roland Huss @@ -69,8 +71,45 @@ public MBeanServerHandler(Configuration pConfig, LogHandler pLogHandler) { mBeanServerManager = new MBeanServerExecutorLocal(detectors); initServerHandle(pConfig, pLogHandler, detectors); initMBean(); + initPlugins(pLogHandler); } + private void initPlugins(LogHandler pLogHandler) { + List plugins = ServiceObjectFactory.createServiceObjects("META-INF/mbean-plugins"); + if (plugins.size() > 0) { + MBeanPluginContext ctx = createMBeanPluginContext(); + for (MBeanPlugin plugin : plugins) { + plugin.init(ctx); + } + } + } + + // Delegate to internal objects + private MBeanPluginContext createMBeanPluginContext() { + return new MBeanPluginContext() { + public ObjectName registerMBean(Object pMBean, String... pOptionalName) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException { + return MBeanServerHandler.this.registerMBean(pMBean,pOptionalName); + } + + public void each(ObjectName pObjectName, MBeanEachCallback pCallback) throws IOException, ReflectionException, MBeanException { + mBeanServerManager.each(pObjectName,pCallback); + } + + public R call(ObjectName pObjectName, MBeanAction pMBeanAction, Object... pExtraArgs) throws IOException, ReflectionException, MBeanException, AttributeNotFoundException, InstanceNotFoundException { + return mBeanServerManager.call(pObjectName,pMBeanAction,pExtraArgs); + } + + public Set queryNames(ObjectName pObjectName) throws IOException { + return mBeanServerManager.queryNames(pObjectName); + } + + public boolean hasMBeansListChangedSince(long pTimestamp) { + return mBeanServerManager.hasMBeansListChangedSince(pTimestamp); + } + }; + } + + /** * Initialize the server handle. * @param pConfig configuration passed through to the server detectors @@ -124,7 +163,7 @@ public final ObjectName registerMBean(Object pMBean,String ... pOptionalName) return registeredName; } catch (RuntimeException exp) { throw new IllegalStateException("Could not register " + pMBean + ": " + exp, exp); - } catch (MBeanRegistrationException exp) { + } catch (MBeanRegistrationException exp) { throw new IllegalStateException("Could not register " + pMBean + ": " + exp, exp); } } @@ -166,7 +205,7 @@ public final void destroy() throws JMException { } // Unregister any notification listener - mBeanServerManager.destroy(); + mBeanServerManager.unregisterFromMBeanNotifications(); } /** @@ -219,6 +258,10 @@ private List lookupDetectors() { return detectors; } + private List lookupMBeanPlugins() { + return ServiceObjectFactory.createServiceObjects("META-INF/mbean-plugins"); + } + // Detect the server by delegating it to a set of predefined detectors. These will be created // by a lookup mechanism, queried and thrown away after this method private ServerHandle detectServers(List pDetectors, LogHandler pLogHandler) { diff --git a/agent/core/src/main/java/org/jolokia/backend/executor/AbstractMBeanServerExecutor.java b/agent/core/src/main/java/org/jolokia/backend/executor/AbstractMBeanServerExecutor.java index ee4738d67..a3aaccf03 100644 --- a/agent/core/src/main/java/org/jolokia/backend/executor/AbstractMBeanServerExecutor.java +++ b/agent/core/src/main/java/org/jolokia/backend/executor/AbstractMBeanServerExecutor.java @@ -138,7 +138,7 @@ protected void registerForMBeanNotifications() { /** * Unregister us as listener from every registered server */ - public void destroy() { + public void unregisterFromMBeanNotifications() { Set servers = getMBeanServers(); Exception lastExp = null; StringBuilder errors = new StringBuilder(); diff --git a/agent/core/src/main/java/org/jolokia/backend/executor/MBeanServerExecutor.java b/agent/core/src/main/java/org/jolokia/backend/executor/MBeanServerExecutor.java index 18c5d281d..8ac139693 100644 --- a/agent/core/src/main/java/org/jolokia/backend/executor/MBeanServerExecutor.java +++ b/agent/core/src/main/java/org/jolokia/backend/executor/MBeanServerExecutor.java @@ -79,12 +79,6 @@ R call(ObjectName pObjectName, MBeanAction pMBeanAction, Object... pExtra */ Set queryNames(ObjectName pObjectName) throws IOException; - /** - * Destructor method. After this method has been called, this executor is out of service and - * must not be used anymore - */ - void destroy(); - /** * Check whether the set of MBeans in all managed MBeanServer has been changed * since the given time. The input is the epoch time in seconds, however, milliseconds diff --git a/agent/core/src/main/java/org/jolokia/backend/plugin/MBeanPlugin.java b/agent/core/src/main/java/org/jolokia/backend/plugin/MBeanPlugin.java new file mode 100644 index 000000000..0514c7428 --- /dev/null +++ b/agent/core/src/main/java/org/jolokia/backend/plugin/MBeanPlugin.java @@ -0,0 +1,37 @@ +package org.jolokia.backend.plugin; + +/* + * + * Copyright 2014 Roland Huss + * + * Licensed 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. + */ + +/** + * Interface describing a plugin which can be used to register extra MBeans for enhancing the Jolokia API. + * MBeanPlugins are looked up from the classpath and should be registered in META-INF/mbean-plugins + * + * @author roland + * @since 12/01/15 + */ +public interface MBeanPlugin { + + /** + * Init method for the plugin. The plugin purpose is to register MBeans and to remember the context so + * that it can be later used for doing JMX calls. The context should be propagated to the registered MBean so + * that it can be reused for JMX lookups during its operation. + * + * @param ctx the context in order to access JMX + */ + void init(MBeanPluginContext ctx); +} diff --git a/agent/core/src/main/java/org/jolokia/backend/plugin/MBeanPluginContext.java b/agent/core/src/main/java/org/jolokia/backend/plugin/MBeanPluginContext.java new file mode 100644 index 000000000..f50c83b5b --- /dev/null +++ b/agent/core/src/main/java/org/jolokia/backend/plugin/MBeanPluginContext.java @@ -0,0 +1,46 @@ +package org.jolokia.backend.plugin;/* + * + * Copyright 2014 Roland Huss + * + * Licensed 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. + */ + +import javax.management.*; + +import org.jolokia.backend.executor.MBeanServerExecutor; + +/** + * A {@link MBeanPlugin}'s context for accessing JMX with all known MBeanServers. Also it allows to register + * on MBeans for offering own functionality. + * + * @author roland + * @since 12/01/15 + */ +public interface MBeanPluginContext extends MBeanServerExecutor { + + /** + * Register a MBean under a certain name to the platform MBeanServer. No neeed to unregister, the MBean will be + * automatically unregistered when shutting down the agent. It is recommended to use the domain jolokia + * and a type type=plugin when registering MBeans. + * + * @param pMBean MBean to register + * @param pOptionalName optional name under which the bean should be registered. If not provided, + * it depends on whether the MBean to register implements {@link javax.management.MBeanRegistration} or + * not. + * + * @return the name under which the MBean is registered. + */ + public ObjectName registerMBean(Object pMBean,String ... pOptionalName) + throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException; + +} diff --git a/agent/core/src/main/java/org/jolokia/detector/ServerHandle.java b/agent/core/src/main/java/org/jolokia/detector/ServerHandle.java index 5f26f7cef..fa25de669 100644 --- a/agent/core/src/main/java/org/jolokia/detector/ServerHandle.java +++ b/agent/core/src/main/java/org/jolokia/detector/ServerHandle.java @@ -25,8 +25,9 @@ import org.jolokia.config.Configuration; import org.jolokia.request.JmxRequest; import org.jolokia.util.LogHandler; +import org.jolokia.util.ServiceObjectFactory; import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; /** * Information about the the server product the agent is running in. @@ -198,14 +199,11 @@ private void addNullSafe(JSONObject pRet, String pKey, Object pValue) { * @return the detector specific configuration */ protected JSONObject getDetectorOptions(Configuration pConfig, LogHandler pLogHandler) { - String optionString = pConfig.get(ConfigKey.DETECTOR_OPTIONS); - if (optionString != null) { - try { - JSONObject opts = (JSONObject) new JSONParser().parse(optionString); - return (JSONObject) opts.get(getProduct()); - } catch (Exception e) { - pLogHandler.error("Could not parse options '" + optionString + "' as JSON object: " + e,e); - } + String options = pConfig.get(ConfigKey.DETECTOR_OPTIONS); + try { + return ServiceObjectFactory.extractServiceConfiguration(options,getProduct()); + } catch (ParseException e) { + pLogHandler.error("Could not parse detector options '" + options + "' as JSON object: " + e,e); } return null; } diff --git a/agent/core/src/main/java/org/jolokia/util/ServiceObjectFactory.java b/agent/core/src/main/java/org/jolokia/util/ServiceObjectFactory.java index 1d37eb129..8b8228505 100644 --- a/agent/core/src/main/java/org/jolokia/util/ServiceObjectFactory.java +++ b/agent/core/src/main/java/org/jolokia/util/ServiceObjectFactory.java @@ -20,6 +20,10 @@ import java.net.URL; import java.util.*; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + /** * A simple factory for creating services with no-arg constructors from a textual * descriptor. This descriptor, which must be a resource loadable by this class' @@ -53,7 +57,7 @@ private ServiceObjectFactory() {} * Normally, default service should be given as first parameter so that custom * descriptors have a chance to remove a default service. * @param type of the service objects to create - * @return a ordered list of created services. + * @return a ordered list of created services or an empty list. */ public static List createServiceObjects(String... pDescriptorPaths) { try { @@ -150,6 +154,22 @@ private static void closeReader(LineNumberReader pReader) { } } + /** + * Helper method for parsing a string as a JSON Object and returns the JSON object which is stored under a certain key + * + * @param serviceConfigString string holding the extra configuration + * @param key key to lookup the extra configuration + * @return the extra configuration or null if none has been given. + * @throws ParseException if parsing of the original string as JSON object fails. + */ + public static JSONObject extractServiceConfiguration(String serviceConfigString, String key) throws ParseException { + if (serviceConfigString != null) { + JSONObject opts = (JSONObject) new JSONParser().parse(serviceConfigString); + return (JSONObject) opts.get(key); + } + return null; + } + // ============================================================================= static class ServiceEntry implements Comparable { diff --git a/agent/core/src/test/java/org/jolokia/backend/executor/AbstractMBeanServerExecutorTest.java b/agent/core/src/test/java/org/jolokia/backend/executor/AbstractMBeanServerExecutorTest.java index e101601b1..0d16924ed 100644 --- a/agent/core/src/test/java/org/jolokia/backend/executor/AbstractMBeanServerExecutorTest.java +++ b/agent/core/src/test/java/org/jolokia/backend/executor/AbstractMBeanServerExecutorTest.java @@ -95,14 +95,14 @@ public void updateChangeTest() throws MalformedObjectNameException, NotCompliant } } } finally { - executor.destroy(); + executor.unregisterFromMBeanNotifications(); } } @Test public void destroyWithoutPriorRegistration() throws NoSuchFieldException, IllegalAccessException { // Should always work, even when no registration has happened. Non exisiting listeners will be simplu ignored, since we didnt do any registration before - executor.destroy(); + executor.unregisterFromMBeanNotifications(); } private long currentTime() { diff --git a/agent/core/src/test/java/org/jolokia/detector/ServerHandleTest.java b/agent/core/src/test/java/org/jolokia/detector/ServerHandleTest.java index c6d19f005..148ab534d 100644 --- a/agent/core/src/test/java/org/jolokia/detector/ServerHandleTest.java +++ b/agent/core/src/test/java/org/jolokia/detector/ServerHandleTest.java @@ -97,7 +97,7 @@ public void detectorOptionsEmpty() { @Test public void detectOptionsFail() { LogHandler handler = EasyMock.createMock(LogHandler.class); - handler.error(matches("^.*parse options.*"),isA(Exception.class)); + handler.error(matches("^.*parse detector options.*"),isA(Exception.class)); replay(handler); Configuration opts = new Configuration(ConfigKey.DETECTOR_OPTIONS,"blub: bla"); diff --git a/agent/core/src/test/java/org/jolokia/handler/ListHandlerTest.java b/agent/core/src/test/java/org/jolokia/handler/ListHandlerTest.java index 27d77f793..81fab6ee1 100644 --- a/agent/core/src/test/java/org/jolokia/handler/ListHandlerTest.java +++ b/agent/core/src/test/java/org/jolokia/handler/ListHandlerTest.java @@ -53,7 +53,7 @@ private void createHandler() { @AfterMethod private void destroy() { - executor.destroy(); + executor.unregisterFromMBeanNotifications(); } @Test diff --git a/agent/core/src/test/java/org/jolokia/util/ServiceObjectFactoryTest.java b/agent/core/src/test/java/org/jolokia/util/ServiceObjectFactoryTest.java index 1e0618b69..a077781e0 100644 --- a/agent/core/src/test/java/org/jolokia/util/ServiceObjectFactoryTest.java +++ b/agent/core/src/test/java/org/jolokia/util/ServiceObjectFactoryTest.java @@ -16,12 +16,15 @@ * limitations under the License. */ -import org.testng.annotations.Test; - import java.util.Iterator; import java.util.List; +import org.json.simple.JSONObject; +import org.json.simple.parser.ParseException; +import org.testng.annotations.Test; + import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; /** * @author roland @@ -53,6 +56,20 @@ public void classCastException() { String bla = services.get(0); } + @Test + public void extractServiceConfiguration() throws ParseException { + String configString = "{ \"healthCheck\" : { \"path\": \"/tmp\"}}"; + JSONObject extra = + ServiceObjectFactory.extractServiceConfiguration(configString, "healthCheck"); + assertEquals("/tmp",extra.get("path")); + assertEquals(extra.size(),1); + assertNull(ServiceObjectFactory.extractServiceConfiguration(configString,"blub")); + } + + @Test(expectedExceptions = ParseException.class) + public void extractServiceConfigurationFailed() throws ParseException { + ServiceObjectFactory.extractServiceConfiguration("{{ blub }", "healthCheck"); + } interface TestService { String getName(); } public static class Test1 implements TestService { public String getName() { return "one"; } }