/*
* #%L
* Nazgul Project: nazgul-core-jmx-test
* %%
* Copyright (C) 2010 - 2017 jGuru Europe AB
* %%
* Licensed under the jGuru Europe AB license (the "License"), based
* on Apache License, Version 2.0; you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
*
* 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.
* #L%
*
*/
package se.jguru.nazgul.core.jmx.test.mbeanserver;
import org.junit.Assert;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import se.jguru.nazgul.core.algorithms.api.Validate;
import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* jUnit rule implementation to manage a local (i.e. in-process) MBeanServer during a Test Case.
* This has significance for all tests requiring a local and controllable MBean server.
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
public class LocalMBeanServerRule extends TestWatcher {
// Shared state
private final Object[] lock = new Object[0];
private SortedSet<String> domains;
private MBeanServer mBeanServer;
private boolean unregisterAllBeansInTestDomain = true;
private String jmxBeanTestDomain;
/**
* Default constructor creating a new LocalMBeanServerRule which unregisters all beans in its TestDomain
* after the test completes (i.e. in the finished() method of this class).
*/
public LocalMBeanServerRule() {
this(true);
}
/**
* Compound constructor creating a new LocalMBeanServerRule which performs automatic de-registration
* of all beans in the test JMX domain.
*
* @param unregisterAllBeansInTestDomain If {@code true}, all beans in the test JMX domain are unregistered
* when the test finishes.
* @see #finished(Description)
*/
public LocalMBeanServerRule(final boolean unregisterAllBeansInTestDomain) {
// Assign internal state
this.unregisterAllBeansInTestDomain = unregisterAllBeansInTestDomain;
}
/**
* <p>Starts a local/in-process MBeanServer, and validates that the JMX test domain is empty (i.e. not
* registered as a domain in the JMX server when the test starts). The test domain is identical to the
* class name of the test class:</p>
* <pre>
* <code>
* // Stash the name of the test JMX domain.
* setJmxBeanTestDomain(description.getClassName());
* </code>
* </pre>
*
* @see #getJmxBeanTestDomain()
* @see #setJmxBeanTestDomain(String)
*/
@Override
protected void starting(final Description description) {
// Acquire the local MBeanServer.
mBeanServer = ManagementFactory.getPlatformMBeanServer();
Assert.assertNotNull("Could not retrieve PlatformMBeanServer.", mBeanServer);
// Stash the name of the test JMX domain.
setJmxBeanTestDomain(description.getClassName());
// Retrieve the existing domains from the mBeanServer.
domains = new TreeSet<>();
getDomains();
// The JMX bean test domain should be empty at the start of the unit test.
Assert.assertFalse("Test domain [" + getJmxBeanTestDomain() + "] already present within local mBeanServer. "
+ "Your unit test state should be cleaned out before launching a JMX test.",
this.domains.contains(getJmxBeanTestDomain()));
}
/**
* Calls {@code mBeanServer.unregisterMBean(objectName);} for each MBean whose objectName starts with the
* jmxBeanTestDomain string - which normally is the same as the fully qualified class name of the test
* class being executed.
*
* @throws MBeanServerUnregisterException if the LocalMBeanServerRule was instructed to unregister all beans in
* the test domain and was unable to do so.
*/
@Override
protected void finished(final Description description) {
final List<Exception> exceptions = new ArrayList<>();
// Remove the test domain and all its sub-elements, if asked to.
if (unregisterAllBeansInTestDomain) {
for (ObjectInstance current : mBeanServer.queryMBeans(null, null)) {
final ObjectName objectName = current.getObjectName();
if (objectName.getDomain().startsWith(getJmxBeanTestDomain())) {
try {
mBeanServer.unregisterMBean(objectName);
} catch (Exception e) {
exceptions.add(e);
}
}
}
}
if (exceptions.size() > 0) {
final MBeanServerUnregisterException ex = new MBeanServerUnregisterException("unregistering MBeans");
ex.getExceptions().addAll(exceptions);
}
}
/**
* Override to provide a JMX domain other than the default test domain.
*
* @return {@code getClass().getPackage().getName()}, unless overridden in subclasses.
*/
public String getJmxBeanTestDomain() {
return jmxBeanTestDomain;
}
/**
* Assigns the JMX bean test domain, which is a domain where test code is applied.
* The JMX bean test domain is normally empty at the start of the unit.
*
* @param jmxBeanTestDomain The JMX bean test domain to assign to the local MBeanServer.
*/
public void setJmxBeanTestDomain(final String jmxBeanTestDomain) {
// Check sanity
Validate.notNull(jmxBeanTestDomain, "jmxBeanTestDomain");
// Assign internal state
this.jmxBeanTestDomain = jmxBeanTestDomain;
}
/**
* @return An unmodifiable SortedSet containing the domains currently registered within the MBean server.
*/
public SortedSet<String> getDomains() {
final String[] currentDomains = mBeanServer.getDomains();
if (currentDomains.length != domains.size()) {
synchronized (lock) {
// Retrieve the existing domains from the mBeanServer.
domains.clear();
Collections.addAll(domains, currentDomains);
}
}
// All done.
return Collections.unmodifiableSortedSet(domains);
}
/**
* Retrieves the value of the attribute with the supplied name from the given ObjectName.
*
* @param objectName The objectName of the MBean whose readable attribute should be retrieved.
* @param attributeName The name of the attribute. <strong>Note!</strong> The attribute should be capitalized,
* implying that a JavaBean getter {@code String getFoo()} implies an attributeName
* "Foo" (not "foo"), according to the JMX specification.
* @param <T> The type of the JMX attribute requested.
* @return The value of the attribute in the supplied objectName.
*/
public final <T> T getAttribute(final ObjectName objectName, final String attributeName) {
try {
return (T) mBeanServer.getAttribute(objectName, attributeName);
} catch (Exception e) {
throw new IllegalArgumentException("Attribute [" + attributeName + "] was not found in objectName ["
+ objectName + "]", e);
}
}
/**
* Retrieves an MBean proxy with the supplied interface type from the local MBeanServer.
*
* @param objectName The objectName of the MBean which must implement the supplied interfaceType.
* @param interfaceType The interface type which should be retrieved.
* @param <T> The interface type to retrieve.
* @return An MBean proxy implementing the supplied interfaceType.
*/
public final <T> T getMXBeanProxy(final ObjectName objectName, final Class<T> interfaceType) {
return JMX.newMXBeanProxy(mBeanServer, objectName, interfaceType, true);
}
/**
* Retrieves all ObjectInstances within the JMX domain supplied.
*
* @param jmxDomain a name of a JMX domain. Cannot be null.
* @return a Set containing all ObjectInstances (i.e. MBeans) within the supplied JMX domain.
*/
public Set<ObjectInstance> getMBeansInDomain(final String jmxDomain) {
return mBeanServer.queryMBeans(getSearchObjectNameFor(jmxDomain), null);
}
/**
* Retrieves all ObjectNames within the JMX domain supplied.
*
* @param jmxDomain a name of a JMX domain. Cannot be null.
* @return a Set containing all ObjectNames within the supplied JMX domain.
*/
public Set<ObjectName> getNamesInDomain(final String jmxDomain) {
return mBeanServer.queryNames(getSearchObjectNameFor(jmxDomain), null);
}
/**
* Retrieves the active MBeanServer from this Rule.
* Normally, this
*
* @return the active MBeanServer from this Rule.
*/
public MBeanServer getMBeanServer() {
return mBeanServer;
}
//
// Private helpers
//
private ObjectName getSearchObjectNameFor(final String jmxDomain) {
// Check sanity
Validate.notNull(jmxDomain, "jmxDomain");
try {
return new ObjectName(jmxDomain + ":*");
} catch (MalformedObjectNameException e) {
throw new IllegalArgumentException("Could not create ObjectName for jmxDomain '" + jmxDomain + "'", e);
}
}
}