/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-10 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id$
*/
package org.exist.management.client;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
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.MBeanServerConnection;
import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.TabularData;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerException;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
import org.exist.dom.QName;
import org.exist.management.impl.SanityReport;
import org.exist.memtree.MemTreeBuilder;
import org.exist.util.ConfigurationHelper;
import org.exist.util.serializer.DOMSerializer;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
/**
* Utility class to output database status information from eXist's JMX interface
* as XML.
*
* @author wolf
*
*/
public class JMXtoXML {
private final static Logger LOG = Logger.getLogger(JMXtoXML.class);
private final static Map<String, ObjectName[]> CATEGORIES = new TreeMap<String, ObjectName[]>();
static {
try {
final ObjectName onMemory = new ObjectName("java.lang:type=Memory");
final ObjectName onRuntime = new ObjectName("java.lang:type=Runtime");
CATEGORIES.put("memory", new ObjectName[] { new ObjectName("java.lang:type=Memory") });
CATEGORIES.put("runtime", new ObjectName[] { new ObjectName("java.lang:type=Runtime") });
CATEGORIES.put("instances", new ObjectName[] { new ObjectName("org.exist.management.*:type=Database") });
CATEGORIES.put("disk", new ObjectName[] { new ObjectName("org.exist.management.*:type=DiskUsage") });
CATEGORIES.put("system", new ObjectName[] { new ObjectName("org.exist.management:type=SystemInfo") });
CATEGORIES.put("caches", new ObjectName[] {
new ObjectName("org.exist.management.exist:type=CacheManager"),
new ObjectName("org.exist.management.exist:type=CollectionCacheManager"),
new ObjectName("org.exist.management.exist:type=CacheManager.Cache,*")
});
CATEGORIES.put("locking", new ObjectName[] { new ObjectName("org.exist.management:type=LockManager") });
CATEGORIES.put("processes", new ObjectName[] { new ObjectName("org.exist.management.*:type=ProcessReport") });
CATEGORIES.put("sanity", new ObjectName[] { new ObjectName("org.exist.management.*.tasks:type=SanityReport") });
CATEGORIES.put("all", new ObjectName[] { new ObjectName("org.exist.*:*"), onMemory, onRuntime });
} catch (MalformedObjectNameException e) {
LOG.warn("Error in initialization: " + e.getMessage(), e);
} catch (NullPointerException e) {
LOG.warn("Error in initialization: " + e.getMessage(), e);
}
}
private final static Properties defaultProperties = new Properties();
static {
defaultProperties.setProperty(OutputKeys.INDENT, "yes");
defaultProperties.setProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
}
public final static String JMX_NAMESPACE = "http://exist-db.org/jmx";
public final static String JMX_PREFIX = "jmx";
private static final QName ROW_ELEMENT = new QName("row", JMX_NAMESPACE, JMX_PREFIX);
public final static QName JMX_ELEMENT = new QName("jmx", JMX_NAMESPACE, JMX_PREFIX);
private static final QName JMX_CONNECTION_ATTR = new QName("connection");
private static final QName JMX_ERROR = new QName("error", JMX_NAMESPACE, JMX_PREFIX);
public static final long PING_TIMEOUT = -99;
private final MBeanServerConnection platformConnection = ManagementFactory.getPlatformMBeanServer();
private MBeanServerConnection connection;
private JMXServiceURL url;
private long ping = -1;
public JMXtoXML() {
}
/**
* Connect to the local JMX instance.
*/
public void connect() {
ArrayList<MBeanServer> servers = MBeanServerFactory.findMBeanServer(null);
if (servers.size() > 0)
connection = servers.get(0);
}
/**
* Connect to a remote JMX instance using address and port.
*
* @param address
* @param port
* @throws IOException
*/
public void connect(String address, int port) throws IOException {
url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://"+address+":" + port + "/jmxrmi");
Map<String, String[]> env = new HashMap<String, String[]>();
String[] creds = {"guest", "guest"};
env.put(JMXConnector.CREDENTIALS, creds);
JMXConnector jmxc = JMXConnectorFactory.connect(url, env);
connection = jmxc.getMBeanServerConnection();
LOG.debug("Connected to JMX server at " + url.toString());
}
/**
* Retrieve JMX output for the given categories and return a string of XML.
* Valid categories are "memory", "instances", "disk", "system", "caches",
* "locking", "processes", "sanity", "all".
*
* @param categories
* @return report
* @throws TransformerException
*/
public String generateReport(String categories[]) throws TransformerException {
Element root = generateXMLReport(null, categories);
StringWriter writer = new StringWriter();
DOMSerializer streamer = new DOMSerializer(writer, defaultProperties);
streamer.serialize(root);
return writer.toString();
}
/**
* Ping the database to see if it is still responsive.
* This will first try to get a database broker object
* and if it succeeds, run a simple query. If the server does
* not respond within the given timeout, the method will return
* an error code -99 ({@link JMXtoXML#PING_TIMEOUT}). If there's an
* error on the server, the return value will be less than 0. Otherwise
* the return value is the response time in milliseconds.
*
* @param instance the name of the database instance (default instance is "exist")
* @param timeout a timeout in milliseconds
* @return Response time in msec, less than 0 in case of an error on server
* or PING_TIMEOUT when server does not respond in time
*/
public long ping(String instance, long timeout) {
ping = SanityReport.PING_WAITING;
long start = System.currentTimeMillis();
Ping thread = new Ping(instance);
thread.start();
synchronized (this) {
while (ping == SanityReport.PING_WAITING) {
try {
wait(100);
} catch (InterruptedException e) {
}
if ((System.currentTimeMillis() - start) >= timeout) {
return PING_TIMEOUT;
}
}
return ping;
}
}
private class Ping extends Thread {
private String instance;
public Ping(String instance) {
this.instance = instance;
}
public void run() {
try {
ObjectName name = new ObjectName("org.exist.management." + instance + ".tasks:type=SanityReport");
ping = (Long) connection.invoke(name, "ping", new Object[] { Boolean.TRUE }, new String[] { boolean.class.getName() });
} catch (Exception e) {
LOG.warn(e.getMessage(), e);
ping = SanityReport.PING_ERROR;
}
synchronized (this) {
notifyAll();
}
}
}
/**
* Retrieve JMX output for the given categories and return it as an XML DOM.
* Valid categories are "memory", "instances", "disk", "system", "caches",
* "locking", "processes", "sanity", "all".
*
* @param errcode an optional error description
* @param categories
* @return xml report
* @throws TransformerException
*/
public Element generateXMLReport(String errcode, String categories[]) {
MemTreeBuilder builder = new MemTreeBuilder();
try {
builder.startDocument();
builder.startElement(JMX_ELEMENT, null);
if (url != null)
builder.addAttribute(JMX_CONNECTION_ATTR, url.toString());
if (errcode != null) {
builder.startElement(JMX_ERROR, null);
builder.characters(errcode);
builder.endElement();
}
for (String category : categories) {
ObjectName[] names = CATEGORIES.get(category);
for (ObjectName name : names) {
queryMBeans(builder, name);
}
}
builder.endElement();
builder.endDocument();
} catch (Exception e) {
e.printStackTrace();
LOG.warn("Could not generate XML report from JMX: " + e.getMessage());
}
return (Element) builder.getDocument().getNode(1);
}
private void queryMBeans(MemTreeBuilder builder, ObjectName query)
throws IOException, InstanceNotFoundException, IntrospectionException, ReflectionException,
SAXException, AttributeNotFoundException, MBeanException, MalformedObjectNameException, NullPointerException {
MBeanServerConnection conn = connection;
Set<ObjectName> beans = conn.queryNames(query, null);
//if the query is not found in the eXist specific MBeans server, then attempt to query the platform for it
if(beans.isEmpty()) {
beans = platformConnection.queryNames(query, null);
conn = platformConnection;
} //TODO examine JUnit source code as alternative method
for (ObjectName name : beans) {
MBeanInfo info = conn.getMBeanInfo(name);
String className = info.getClassName();
int p = className.lastIndexOf('.');
if (p > -1 && p + 1 < className.length())
className = className.substring(p + 1);
QName qname = new QName(className, JMX_NAMESPACE, JMX_PREFIX);
builder.startElement(qname, null);
builder.addAttribute(new QName("name"), name.toString());
MBeanAttributeInfo[] beanAttribs = info.getAttributes();
for (int i = 0; i < beanAttribs.length; i++) {
if (beanAttribs[i].isReadable()) {
try {
QName attrQName = new QName(beanAttribs[i].getName(), JMX_NAMESPACE, JMX_PREFIX);
Object attrib = conn.getAttribute(name, beanAttribs[i].getName());
builder.startElement(attrQName, null);
serializeObject(builder, attrib);
builder.endElement();
} catch (Exception e) {
LOG.debug("exception caught: " + e.getMessage(), e);
}
}
}
builder.endElement();
}
}
private void serializeObject(MemTreeBuilder builder, Object object) throws SAXException {
if (object == null)
return;
if (object instanceof TabularData)
serialize(builder, (TabularData) object);
else if (object instanceof CompositeData)
serialize(builder, (CompositeData) object);
else if (object instanceof Object[])
serialize(builder, (Object[]) object);
else
builder.characters(object.toString());
}
private void serialize(MemTreeBuilder builder, Object[] data) throws SAXException {
for (Object o : data) {
serializeObject(builder, o);
}
}
private void serialize(MemTreeBuilder builder, CompositeData data) throws SAXException {
CompositeType type = data.getCompositeType();
for (Object tmp : type.keySet()) {
String key = (String) tmp;
QName qname = new QName(key, JMX_NAMESPACE, JMX_PREFIX);
builder.startElement(qname, null);
serializeObject(builder, data.get(key));
builder.endElement();
}
}
private void serialize(MemTreeBuilder builder, TabularData data) throws SAXException {
CompositeType rowType = data.getTabularType().getRowType();
for (Object rowObj : data.values()) {
CompositeData row = (CompositeData) rowObj;
builder.startElement(ROW_ELEMENT, null);
for (Object tmp : rowType.keySet()) {
String key = (String) tmp;
Object columnData = row.get(key);
QName columnQName = new QName(key, JMX_NAMESPACE, JMX_PREFIX);
builder.startElement(columnQName, null);
serializeObject(builder, columnData);
builder.endElement();
}
builder.endElement();
}
}
/**
* @param args
*/
public static void main(String[] args) {
File home = ConfigurationHelper.getExistHome();
DOMConfigurator.configure(new File(home, "log4j.xml").getAbsolutePath());
JMXtoXML client = new JMXtoXML();
try {
client.connect("localhost", 1099);
System.out.println(client.generateReport(args));
} catch (IOException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
}
}
}