/**
* Copyright (c) 2002-2012 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jmx;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.lang.management.ManagementFactory;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.management.Descriptor;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.factory.HighlyAvailableGraphDatabaseFactory;
import org.neo4j.ha.CreateEmptyDb;
import org.neo4j.kernel.ha.HaSettings;
import org.neo4j.test.AsciiDocGenerator;
import org.neo4j.test.TargetDirectory;
public class JmxDocsTest
{
private static final String IFDEF_HTMLOUTPUT = "ifndef::nonhtmloutput[]\n";
private static final String IFDEF_NONHTMLOUTPUT = "ifdef::nonhtmloutput[]\n";
private static final String ENDIF = "endif::nonhtmloutput[]\n";
private static final String BEAN_NAME0 = "name0";
private static final String BEAN_NAME = "name";
private static final List<String> QUERIES = Arrays.asList( new String[]{"org.neo4j:*"} );
private static final String JAVADOC_URL = "http://components.neo4j.org/neo4j-enterprise/{neo4j-version}/apidocs/";
private static final int EXPECTED_NUMBER_OF_BEANS = 13;
private static final Set<String> EXCLUDES = new HashSet<String>()
{
{
add( "JMX Server" );
}
};
private static final Map<String, String> TYPES = new HashMap<String, String>()
{
{
put( "java.lang.String", "String" );
put( "java.util.List", "List (java.util.List)" );
put( "java.util.Date", "Date (java.util.Date)" );
}
};
private static final TargetDirectory dir = TargetDirectory.forTest( JmxDocsTest.class );
private static GraphDatabaseService db;
@BeforeClass
public static void startDb() throws Exception
{
File storeDir = dir.graphDbDir( /*clean=*/true );
CreateEmptyDb.at( storeDir );
db = new HighlyAvailableGraphDatabaseFactory().
newHighlyAvailableDatabaseBuilder( storeDir.getAbsolutePath() )
.setConfig( HaSettings.server_id, "1" ).setConfig( "jmx.port", "9913" ).newGraphDatabase();
}
@AfterClass
public static void stopDb() throws Exception
{
if ( db != null )
{
db.shutdown();
}
db = null;
dir.cleanup();
}
@Test
public void dumpJmxInfo() throws Exception
{
StringBuilder beanList = new StringBuilder( 4096 );
StringBuilder altBeanList = new StringBuilder( 2048 );
altBeanList.append( IFDEF_NONHTMLOUTPUT );
beanList.append( "[[jmx-list]]\n" + ".MBeans exposed by Neo4j\n"
+ IFDEF_HTMLOUTPUT
+ "[options=\"header\", cols=\"m,\"]\n" + "|===\n"
+ "|Name|Description\n" );
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
SortedMap<String, ObjectName> neo4jBeans = new TreeMap<String, ObjectName>(
String.CASE_INSENSITIVE_ORDER );
for ( String query : QUERIES )
{
Set<ObjectInstance> beans = mBeanServer.queryMBeans(
new ObjectName( query ), null );
for ( ObjectInstance bean : beans )
{
ObjectName objectName = bean.getObjectName();
String name = objectName.getKeyProperty( BEAN_NAME );
if ( EXCLUDES.contains( name ) )
{
continue;
}
String name0 = objectName.getKeyProperty( BEAN_NAME0 );
if ( name0 != null )
{
name += '/' + name0;
}
neo4jBeans.put( name, bean.getObjectName() );
}
}
assertEquals( "Sanity checking the number of beans found;",
EXPECTED_NUMBER_OF_BEANS, neo4jBeans.size() );
for ( Map.Entry<String, ObjectName> beanEntry : neo4jBeans.entrySet() )
{
ObjectName objectName = beanEntry.getValue();
String name = beanEntry.getKey();
Set<ObjectInstance> mBeans = mBeanServer.queryMBeans( objectName,
null );
if ( mBeans.size() != 1 )
{
throw new IllegalStateException( "Unexpected size ["
+ mBeans.size()
+ "] of query result for ["
+ objectName + "]." );
}
ObjectInstance bean = mBeans.iterator()
.next();
MBeanInfo info = mBeanServer.getMBeanInfo( objectName );
String description = info.getDescription()
.replace( '\n', ' ' );
String id = getId( name );
beanList.append( "|<<" )
.append( id )
.append( ',' )
.append( name )
.append( ">>|" )
.append( description )
.append( '\n' );
altBeanList.append( "* <<" )
.append( id )
.append( ',' )
.append( name )
.append( ">>: " )
.append( description )
.append( '\n' );
writeDetailsToFile( id, objectName, bean, info, description );
}
beanList.append( "|===\n" )
.append( ENDIF );
altBeanList.append( ENDIF )
.append( "\n" );
beanList.append( altBeanList.toString() );
Writer fw = null;
try
{
fw = AsciiDocGenerator.getFW( "target/docs/ops", "JMX List" );
fw.write( beanList.toString() );
}
finally
{
if ( fw != null )
{
fw.close();
}
}
}
private String getId( String name )
{
return "jmx-" + name.replace( ' ', '-' )
.replace( '/', '-' )
.toLowerCase();
}
private void writeDetailsToFile( String id, ObjectName objectName,
ObjectInstance bean, MBeanInfo info, String description )
throws IOException
{
StringBuilder beanInfo = new StringBuilder( 2048 );
String name = objectName.getKeyProperty( BEAN_NAME );
String name0 = objectName.getKeyProperty( BEAN_NAME0 );
if ( name0 != null )
{
name += "/" + name0;
}
MBeanAttributeInfo[] attributes = info.getAttributes();
if ( attributes.length > 0 )
{
beanInfo.append( "[[" )
.append( id )
.append( "]]\n" + ".MBean " )
.append( name )
.append( " (" )
.append( bean.getClassName() )
.append( ") Attributes\n" );
writeAttributesTable( description, beanInfo, attributes, false );
writeAttributesTable( description, beanInfo, attributes, true );
beanInfo.append( "\n" );
}
MBeanOperationInfo[] operations = info.getOperations();
if ( operations.length > 0 )
{
beanInfo.append( ".MBean " )
.append( name )
.append( " (" )
.append( bean.getClassName() )
.append( ") Operations\n" );
writeOperationsTable( beanInfo, operations, false );
writeOperationsTable( beanInfo, operations, true );
beanInfo.append( "\n" );
}
if ( beanInfo.length() > 0 )
{
Writer fw = null;
try
{
fw = AsciiDocGenerator.getFW( "target/docs/ops", id );
fw.write( beanInfo.toString() );
}
finally
{
if ( fw != null )
{
fw.close();
}
}
}
}
private void writeAttributesTable( String description,
StringBuilder beanInfo, MBeanAttributeInfo[] attributes,
boolean nonHtml )
{
addNonHtmlCondition( beanInfo, nonHtml );
beanInfo.append(
"[options=\"header\", cols=\"20m,36,20m,7,7\"]\n" + "|===\n"
+ "|Name|Description|Type|Read|Write\n" + "5.1+^e|" )
.append( description )
.append( '\n' );
SortedSet<String> attributeInfo = new TreeSet<String>(
String.CASE_INSENSITIVE_ORDER );
for ( MBeanAttributeInfo attrInfo : attributes )
{
StringBuilder attributeRow = new StringBuilder( 512 );
String type = getType( attrInfo.getType() );
Descriptor descriptor = attrInfo.getDescriptor();
type = getCompositeType( type, descriptor, nonHtml );
attributeRow.append( '|' )
.append( makeBreakable( attrInfo.getName(), nonHtml ) )
.append( '|' )
.append( attrInfo.getDescription()
.replace( '\n', ' ' ) )
.append( '|' )
.append( type )
.append( '|' )
.append( attrInfo.isReadable() ? "yes" : "no" )
.append( '|' )
.append( attrInfo.isWritable() ? "yes" : "no" )
.append( '\n' );
attributeInfo.add( attributeRow.toString() );
}
for ( String row : attributeInfo )
{
beanInfo.append( row );
}
beanInfo.append( "|===\n" );
beanInfo.append( ENDIF );
}
private void addNonHtmlCondition( StringBuilder beanInfo, boolean nonHtml )
{
if ( nonHtml )
{
beanInfo.append( IFDEF_NONHTMLOUTPUT );
}
else
{
beanInfo.append( IFDEF_HTMLOUTPUT );
}
}
private void writeOperationsTable( StringBuilder beanInfo,
MBeanOperationInfo[] operations, boolean nonHtml )
{
addNonHtmlCondition( beanInfo, nonHtml );
beanInfo.append( "[options=\"header\", cols=\"20m,40,20m,20m\"]\n"
+ "|===\n"
+ "|Name|Description|ReturnType|Signature\n" );
SortedSet<String> operationInfo = new TreeSet<String>(
String.CASE_INSENSITIVE_ORDER );
for ( MBeanOperationInfo operInfo : operations )
{
StringBuilder operationRow = new StringBuilder( 512 );
String type = getType( operInfo.getReturnType() );
Descriptor descriptor = operInfo.getDescriptor();
type = getCompositeType( type, descriptor, nonHtml );
operationRow.append( '|' )
.append( operInfo.getName() )
.append( '|' )
.append( operInfo.getDescription()
.replace( '\n', ' ' ) )
.append( '|' )
.append( type )
.append( '|' );
MBeanParameterInfo[] params = operInfo.getSignature();
if ( params.length > 0 )
{
for ( int i = 0; i < params.length; i++ )
{
MBeanParameterInfo param = params[i];
operationRow.append( param.getType() );
if ( i != (params.length - 1) )
{
operationRow.append( ',' );
}
}
}
else
{
operationRow.append( "(no parameters)" );
}
operationRow.append( '\n' );
operationInfo.add( operationRow.toString() );
}
for ( String row : operationInfo )
{
beanInfo.append( row );
}
beanInfo.append( "|===\n" );
beanInfo.append( ENDIF );
}
private String getCompositeType( String type, Descriptor descriptor,
boolean nonHtml )
{
String newType = type;
if ( "javax.management.openmbean.CompositeData[]".equals( type ) )
{
Object originalType = descriptor.getFieldValue( "originalType" );
if ( originalType != null )
{
newType = getLinkedType( getType( (String) originalType ),
nonHtml );
if ( nonHtml )
{
newType += " as CompositeData[]";
}
else
{
newType += " as http://docs.oracle.com/javase/6/docs/api/javax/management/openmbean/CompositeData" +
".html[CompositeData][]";
}
}
}
return newType;
}
private String getType( String type )
{
if ( TYPES.containsKey( type ) )
{
return TYPES.get( type );
}
else if ( type.endsWith( ";" ) )
{
if ( type.startsWith( "[L" ) )
{
return type.substring( 2, type.length() - 1 ) + "[]";
}
else
{
throw new IllegalArgumentException(
"Don't know how to parse this type: " + type );
}
}
return type;
}
private String getLinkedType( String type, boolean nonHtml )
{
if ( !type.startsWith( "org.neo4j" ) )
{
if ( !type.startsWith( "java.util.List<org.neo4j." ) )
{
return type;
}
else
{
String typeInList = type.substring( 15, type.length() - 1 );
return "java.util.List<" + getLinkedType( typeInList, nonHtml )
+ ">";
}
}
else if ( nonHtml )
{
return type;
}
else
{
StringBuilder url = new StringBuilder( 160 );
url.append( JAVADOC_URL );
String typeString = type;
if ( type.endsWith( "[]" ) )
{
typeString = type.substring( 0, type.length() - 2 );
}
url.append( typeString.replace( '.', '/' ) )
.append( ".html[" )
.append( typeString )
.append( "]" );
if ( type.endsWith( "[]" ) )
{
url.append( "[]" );
}
return url.toString();
}
}
private String makeBreakable( String name, boolean nonHtml )
{
if ( nonHtml )
{
return name.replace( "_", "_\u200A" )
.replace( "NumberOf", "NumberOf\u200A" )
.replace( "InUse", "\u200AInUse" )
.replace( "Transactions", "\u200ATransactions" );
}
else
{
return name;
}
}
}