SimpleMBean.java

package com.ziesemer.utils.jmx.server;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.ReflectionException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>A high-performance, &quot;simpler&quot; replacement to
 * 	{@link javax.management.StandardMBean}.</p>
 * <p>Unlike {@link javax.management.StandardMBean}, this class does not implement
 * 	{@link javax.management.MBeanRegistration}.  However, there is nothing
 * 	to prevent a subclass from doing so, which is encouraged if necessary.</p>
 * <p>This class is written rather &quot;fail-fast&quot;.
 * 	{@link Method}s for getters, setters, and operations are all initialized
 * 		during instance creation, then reused for performance over the lifetime of the bean.
 * 	Any exceptions thrown during this process will be thrown immediately,
 * 		causing the constructor to fail and no instance created.
 * 	This is preferred over having a partially-functional bean.</p>
 * @author Mark A. Ziesemer
 * 	<a href="http://www.ziesemer.com.">&lt;www.ziesemer.com&gt;</a>
 */
public class SimpleMBean implements DynamicMBean{
	
	protected static final Logger LOGGER = LoggerFactory.getLogger(SimpleMBean.class);
	
	// Similar to code used in java.io.ObjectInputStream.
	// http://java.sun.com./docs/books/tutorial/java/nutsandbolts/datatypes.html
	protected static final Map<String, Class<?>> PRIMITIVE_CLASSES;
	static{
		final List<Class<?>> pcl = Arrays.<Class<?>>asList(
			boolean.class, byte.class, char.class, short.class, int.class,
			long.class, float.class, double.class, void.class);
		final Map<String, Class<?>> pc = new HashMap<String, Class<?>>(pcl.size(), 1.0F);
		for(final Class<?> c : pcl){
			pc.put(c.getName(), c);
		}
		PRIMITIVE_CLASSES = Collections.unmodifiableMap(pc);
	}
	
	protected Object implementation;
	protected final Class<?> mBeanInterface;
	protected MBeanInfo mBeanInfo;
	
	protected final Map<String, Method> getters = new HashMap<String, Method>();
	protected final Map<String, Method> setters = new HashMap<String, Method>();
	/**
	 * Map Tree: method name, # of parameters.
	 */
	protected final Map<String, Map<Integer, List<MethodAndSig>>> operations
		= new HashMap<String, Map<Integer, List<MethodAndSig>>>();
	
	public <T> SimpleMBean(final Object implementation, final Class<T> mBeanInterface) throws JMException{
		this.mBeanInterface = mBeanInterface;
		this.mBeanInfo = buildMBeanInfo();
		setImplementation(implementation);
		buildAttributes();
		buildOperations();
	}
	
	protected MBeanInfo buildMBeanInfo() throws JMException{
		return new MBeanInfoBuilder().build(mBeanInterface);
	}
	
	protected void buildAttributes() throws JMException{
		try{
			for(final MBeanAttributeInfo attribute : mBeanInfo.getAttributes()){
				final String name = attribute.getName();
				if(attribute.isReadable()){
					getters.put(name,
						mBeanInterface.getMethod(
							attribute.isIs() ? "is" + name : "get" + name));
				}
				if(attribute.isWritable()){
					setters.put(name,
						mBeanInterface.getMethod(
							"set" + name, new Class[]{getClassFromType(attribute.getType())}));
				}
			}
		}catch(final ClassNotFoundException cnfe){
			throw new ReflectionException(cnfe);
		}catch(final NoSuchMethodException nsme){
			throw new ReflectionException(nsme);
		}
	}
	
	protected void buildOperations() throws JMException{
		try{
			for(final MBeanOperationInfo operation : mBeanInfo.getOperations()){
				final String actionName = operation.getName();
				final MBeanParameterInfo[] beanSignature = operation.getSignature();
				final int signatureLength = beanSignature.length;
				final Class<?>[] signature = new Class[signatureLength];
				final String[] signatureStr = new String[signatureLength];
				for(int i=0; i < signatureLength; i++){
					final String typeName = beanSignature[i].getType();
					signature[i] = getClassFromType(typeName);
					signatureStr[i] = typeName;
				}
				
				final MethodAndSig mas = new MethodAndSig();
				mas.method = mBeanInterface.getMethod(actionName, signature);
				mas.signature = signatureStr;
				
				Map<Integer, List<MethodAndSig>> ol = operations.get(actionName);
				if(ol == null){
					ol = new HashMap<Integer, List<MethodAndSig>>();
					operations.put(actionName, ol);
				}
				List<MethodAndSig> olc = ol.get(signatureLength);
				if(olc == null){
					olc = new LinkedList<MethodAndSig>();
					ol.put(signatureLength, olc);
				}
				olc.add(mas);
			}
		}catch(final ClassNotFoundException cnfe){
			throw new ReflectionException(cnfe);
		}catch(final NoSuchMethodException nsme){
			throw new ReflectionException(nsme);
		}
	}
	
	public MBeanInfo getMBeanInfo(){
		return mBeanInfo;
	}
	
	public Object getAttribute(final String attribute)
			throws AttributeNotFoundException, MBeanException, ReflectionException{
		final Method m = getters.get(attribute);
		if(m == null){
			final AttributeNotFoundException anfe = new AttributeNotFoundException(
				"\"" + attribute + "\" not read accessible.");
			logClientException(anfe.getMessage());
			throw anfe;
		}
		try{
			return m.invoke(implementation);
		}catch(final InvocationTargetException ite){
			throw new ReflectionException(ite);
		}catch(final IllegalAccessException iae){
			throw new ReflectionException(iae);
		}
	}
	
	public AttributeList getAttributes(final String[] attributes){
		final AttributeList result = new AttributeList(attributes.length);
		for(final String attribute : attributes){
			try{
				result.add(new Attribute(attribute, getAttribute(attribute)));
			}catch(final AttributeNotFoundException anfe){
				// Already logged by child call.
			}catch(final Exception ex){
				LOGGER.warn(ex.getMessage(), ex);
			}
		}
		return result;
	}
	
	public void setAttribute(final Attribute attribute)
			throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException{
		if(attribute == null){
			throw new IllegalArgumentException("attribute cannot be null.");
		}
		final Method m = setters.get(attribute.getName());
		if(m == null){
			final AttributeNotFoundException anfe = new AttributeNotFoundException(
				"\"" + attribute.getName() + "\" not write accessible.");
			logClientException(anfe.getMessage());
			throw anfe;
		}
		try{
			m.invoke(implementation, attribute.getValue());
		}catch(final InvocationTargetException ite){
			throw new ReflectionException(ite);
		}catch(final IllegalAccessException iae){
			throw new ReflectionException(iae);
		}
	}
	
	public AttributeList setAttributes(final AttributeList attributes){
		final AttributeList result = new AttributeList(attributes.size());
		for(final Object attributeObj : attributes){
			// AttributeList should use generics when extending ArrayList.
			final Attribute attribute = (Attribute)attributeObj;
			try{
				setAttribute(attribute);
				result.add(attribute);
			}catch(final AttributeNotFoundException anfe){
				// Already logged by child call.
			}catch(final Exception ex){
				LOGGER.warn(ex.getMessage(), ex);
			}
		}
		return result;
	}
	
	public Object invoke(final String actionName, final Object[] params, final String[] signature)
			throws MBeanException, ReflectionException{
		final Map<Integer, List<MethodAndSig>> ol = operations.get(actionName);
		if(ol == null){
			throw createNoSuchMethodReflectionException(
				"The operation with name \"" + actionName + "\" could not be found.");
		}
		
		final int signatureLength = signature == null ? 0 : signature.length;
		final List<MethodAndSig> olc = ol.get(signatureLength);
		if(olc == null){
			throw createNoSuchMethodReflectionException(
				"The operation with name \"" + actionName + "\" and " + signatureLength + " parameters could not be found.");
		}
		
		Method m = null;
		for(final MethodAndSig mas : olc){
			if(Arrays.equals(mas.signature, signature)
					|| (signatureLength == 0 && mas.signature.length == 0)){
				m = mas.method;
				break;
			}
		}
		if(m == null){
			throw createNoSuchMethodReflectionException(
				"The operation with name \"" + actionName + "\" and signature "
				+ (signature == null ? null : Arrays.asList(signature)) + " could not be found.");
		}
		
		try{
			return m.invoke(implementation, params);
		}catch(final InvocationTargetException ite){
			LOGGER.warn(ite.toString(), ite);
			throw new ReflectionException(ite, ite.getMessage());
		}catch(final IllegalAccessException iae){
			LOGGER.warn(iae.toString(), iae);
			throw new ReflectionException(iae, iae.getMessage());
		}
	}
	
	protected ReflectionException createNoSuchMethodReflectionException(final String msg){
		final NoSuchMethodException nsme = new NoSuchMethodException(msg);
		logClientException(msg);
		return new ReflectionException(nsme, msg);
	}
	
	public Object getImplementation(){
		return implementation;
	}
	
	public void setImplementation(final Object implementation){
		if(!mBeanInterface.isInstance(implementation)){
			if(implementation == null){
				throw new IllegalArgumentException("Implementation cannot be null.");
			}
			throw new IllegalArgumentException("Implementation must match interface.");
		}
		
		this.implementation = implementation;
		// Update implementation class name in MBeanInfo
		this.mBeanInfo = new MBeanInfo(implementation.getClass().getName(),
			mBeanInfo.getDescription(), mBeanInfo.getAttributes(),
			mBeanInfo.getConstructors(), mBeanInfo.getOperations(),
			mBeanInfo.getNotifications());
	}
	
	public Class<?> getMBeanInterface(){
		return mBeanInterface;
	}
	
	protected Class<?> getClassFromType(final String name) throws ClassNotFoundException{
		final Class<?> c = PRIMITIVE_CLASSES.get(name);
		if(c != null){
			return c;
		}
		return Class.forName(name);
	}
	
	protected void logClientException(final Object msg){
		LOGGER.warn("{}: {}", implementation.getClass().getName(), msg);
	}
	
	protected static class MethodAndSig{
		public Method method;
		public String[] signature;
	}

}