/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included  with this distribution in
 * the LICENSE.txt file.
 */
package org.apache.avalon.phoenix.components.manager;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.io.InputStream;
import java.util.ArrayList;
import javax.management.MBeanParameterInfo;
import javax.management.Descriptor;
import javax.management.modelmbean.DescriptorSupport;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import org.apache.avalon.excalibur.i18n.ResourceManager;
import org.apache.avalon.excalibur.i18n.Resources;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.phoenix.tools.configuration.ConfigurationBuilder;
import org.xml.sax.InputSource;


/**
 * An MBeanInfoBuilder is responsible for building <code>ManagementTopic</code>
 * objects from Configuration objects. The format for Configuration object
 * is specified in the MxInfo specification.  The information is loaded into
 * the Target structure.
 *
 * @author <a href="mailto:peter@apache.org">Peter Donald</a>
 * @author <a href="mailto:huw@mmlive.com">Huw Roberts</a>
 * @version $Revision: 1.16 $ $Date: 2002/05/15 12:11:13 $
 */
public final class MBeanInfoBuilder extends AbstractLogEnabled
{
    private static final Resources REZ =
        ResourceManager.getPackageResources( MBeanInfoBuilder.class );

    public void build(  final Target target, 
                        final Class managedClass,
                        final Class[] interfaces ) 
        throws ConfigurationException
    {
        getLogger().debug( REZ.getString( "mxinfo.debug.building", managedClass.getName() ) );

        // if the managed class has an mxinfo file, build the target from it
        // (this includes any proxies)
        Configuration config = loadMxInfo( managedClass );
        
        if (config != null) 
        {
            getLogger().debug( REZ.getString( "mxinfo.debug.found.mxinfo", managedClass.getName() ) );
            buildFromMxInfo( target, managedClass, config );
        }
        
        // for each interface, generate a topic from its mxinfo file
        // or through introspection
        for (int i = 0, j = interfaces.length ; i < j ; i++ )
        {
            try 
            {
                config = loadMxInfo( interfaces[i] );
                if (config == null) 
                {
                    buildFromIntrospection( target, managedClass, interfaces[i] );
                }
                else 
                {
                    buildFromMxInfo( target, managedClass, config );
                }
            }
            catch (Exception e) 
            {
                final String message =
                    REZ.getString( "mxinfo.error.target", target.getName() );
                getLogger().error( message, e );
                throw new ConfigurationException( message );
            }
        }
    }
    
    /**
     * Create a <code>ModelMBeanInfoSupport</code> object for specified classname from
     * specified configuration data.
     *
     * @param target
     * @param managedClass
     * @param config
     * @throws Exception  
     */
    private void buildFromMxInfo(   final Target target, 
                                    final Class managedClass, 
                                    final Configuration config )
        throws ConfigurationException
    {
        BeanInfo beanInfo;
        try {
            beanInfo = Introspector.getBeanInfo( managedClass );        
        }
        catch (Exception e) 
        {
            throw new ConfigurationException( 
                REZ.getString( "mxinfo.error.introspect", managedClass.getName() ) );
        }

        // load each topic
        Configuration[] topicsConfig = config.getChildren( "topic" );
        for (int i=0; i < topicsConfig.length; i++) 
        {
            ModelMBeanInfoSupport topic = buildTopic( topicsConfig[i], beanInfo );
            target.addTopic(topic);
        }
        
        // load each proxy
        Configuration[] proxysConfig = config.getChildren( "proxy" );
        for (int i=0; i < proxysConfig.length; i++) 
        {
            ModelMBeanInfoSupport topic = buildProxyTopic( proxysConfig[i], managedClass );
            target.addTopic(topic);
        }
        
    }

    /**
     * Builds a topic based on introspection of the interface
     *
     * @param target
     * @param managedClass
     * @param interfaceClass
     * @throws ConfigurationException  
     */
    private void buildFromIntrospection( Target target, 
                                    Class managedClass, 
                                    Class interfaceClass )
        throws ConfigurationException
    {
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo( interfaceClass );        

            // do the methods
            final MethodDescriptor[] methods = beanInfo.getMethodDescriptors();
            ArrayList operations = new ArrayList();
            
            for( int j = 0; j < methods.length; j++ )
            {
                // skip getters and setters
                if ( ! ( methods[j].getName().startsWith("get") ||
                         methods[j].getName().startsWith("set") ||
                         methods[j].getName().startsWith("is") ) )
                {
                    operations.add( buildOperationInfo( methods[j], null ) );
                }
            }
            
            ModelMBeanOperationInfo[] operationList = 
                (ModelMBeanOperationInfo[])
                operations.toArray( new ModelMBeanOperationInfo[0] );
            
            // do the attributes
            final PropertyDescriptor[] propertys = beanInfo.getPropertyDescriptors();
            ModelMBeanAttributeInfo[] attributeList = 
                new ModelMBeanAttributeInfo[propertys.length];
            
            for( int j = 0; j < propertys.length; j++ )
            {
                attributeList[j] = buildAttributeInfo( propertys[j], null );
            }

            final ModelMBeanConstructorInfo[] constructorList = 
                new ModelMBeanConstructorInfo[0];

            final ModelMBeanNotificationInfo[] notificationList = 
                new ModelMBeanNotificationInfo[0];

            final ModelMBeanInfoSupport topic 
                = new ModelMBeanInfoSupport( 
                    "javax.management.modelmbean.RequiredModelMBean", 
                    getShortName( interfaceClass.getName() ), 
                    attributeList, constructorList, operationList, notificationList );
            
            // add it to target
            getLogger().debug( 
                REZ.getString( "mxinfo.debug.adding.topic", topic.getDescription() ) );
            target.addTopic(topic);
    
        }
        catch (Exception e) {
            e.printStackTrace();
            throw new ConfigurationException( 
                REZ.getString( "mxinfo.error.topic", interfaceClass ) );
        }
    }   
    
    /**
     * A utility method to build a <code>ModelMBeanInfoSupport</code>
     * object from specified configuration and BeanInfo.
     *
     * @return the created ModelMBeanInfoSupport
     * @param topicConfig
     * @throws ConfigurationException if an error occurs 
     */
    private ModelMBeanInfoSupport buildTopic( final Configuration topicConfig, BeanInfo beanInfo )
        throws ConfigurationException
    {
       
        final ModelMBeanAttributeInfo[] attributeList = 
            buildAttributeInfos( topicConfig, beanInfo );
        
        final ModelMBeanOperationInfo[] operationList =
            buildOperationInfos( topicConfig, beanInfo );
        
        final ModelMBeanConstructorInfo[] constructorList = 
            new ModelMBeanConstructorInfo[0];

        final ModelMBeanNotificationInfo[] notificationList = 
            new ModelMBeanNotificationInfo[0];
        
        final ModelMBeanInfoSupport topic 
            = new ModelMBeanInfoSupport( 
                "javax.management.modelmbean.RequiredModelMBean", 
                topicConfig.getAttribute("name"), 
                attributeList, constructorList, operationList, notificationList );
       
        return topic;
    }
   
    /**
     * Build a topic for a proxy management class
     *
     * @param proxyTagConfig
     * @param managedClass
     * @return  
     */    
    private ModelMBeanInfoSupport buildProxyTopic( final Configuration proxyTagConfig, 
                                                   final Class managedClass ) 
        throws ConfigurationException
    {
        try {
            String proxyName = proxyTagConfig.getAttribute( "name" );
            getLogger().debug( 
                REZ.getString( "mxinfo.debug.building.proxy.topic", proxyName ) );
            Class proxyClass = managedClass.getClassLoader().loadClass( proxyName );
            Configuration classConfig = loadMxInfo( proxyClass );
            Configuration topicConfig = classConfig.getChild( "topic" );

            BeanInfo beanInfo = Introspector.getBeanInfo( proxyClass );        
            
            final ModelMBeanInfoSupport topic = buildTopic( topicConfig, beanInfo );
            
            Descriptor mBeanDescriptor = topic.getMBeanDescriptor();
            mBeanDescriptor.setField( "proxyClassName", proxyName ) ;
            topic.setMBeanDescriptor( mBeanDescriptor );

            return topic;
        }
        catch (Exception e) 
        {
            if (e instanceof ConfigurationException) 
            {
                throw (ConfigurationException) e;
            }
            else {
                throw new ConfigurationException( 
                    REZ.getString( "mxinfo.error.proxy", managedClass.getName() ) );
            }
        }
    }
    
    /**
     * Builds the management attributes from the configuration
     *
     * @param topicConfig topic's configuration element
     * @param beanInfo managed class' BeanInfo from introspector
     * @throws ConfigurationException  
     */    
    private ModelMBeanAttributeInfo[] buildAttributeInfos( 
            final Configuration topicConfig, final BeanInfo beanInfo ) 
       throws ConfigurationException
    {

        final Configuration[] attributesConfig = topicConfig.getChildren( "attribute" );
        
        ModelMBeanAttributeInfo[] attributeList = 
            new ModelMBeanAttributeInfo[attributesConfig.length];
        
        final PropertyDescriptor[] propertyDescriptorList = beanInfo.getPropertyDescriptors();
        
        for( int i = 0; i < attributesConfig.length; i++ )
        {
            attributeList[i] = buildAttributeInfo( 
                getPropertyDescriptor( 
                    attributesConfig[i].getAttribute("name"), 
                    propertyDescriptorList ),
                attributesConfig[i] );
        }
        
        return attributeList;
    }

    /**
     * Builds a management attribute
     *
     * @param propertyDescriptor from BeanInfo
     * @param attribute's configuration element - can be null, in which case defaults are used
     * @throws ConfigurationException
     * @return  
     */    
    private ModelMBeanAttributeInfo buildAttributeInfo( 
            PropertyDescriptor propertyDescriptor,
            Configuration attributeConfig )
       throws ConfigurationException
    {
        final String name = propertyDescriptor.getName();
        final Method readMethod = propertyDescriptor.getReadMethod();
        final Method writeMethod = propertyDescriptor.getWriteMethod();
        final String type = propertyDescriptor.getPropertyType().getName();

        String description;
        boolean isReadable;
        boolean isWriteable;
        
        if (attributeConfig == null) 
        {
            // if no config then use the BeanInfo
            description = propertyDescriptor.getDisplayName();
            isReadable = (readMethod != null);
            isWriteable = (writeMethod != null);
        }
        else 
        {
            // use config info, or BeanInfo if config info is missing
            description = attributeConfig.getAttribute( "description", propertyDescriptor.getDisplayName() );

            // defaults to true if there is a read method, otherwise defaults to false
            isReadable = attributeConfig.getAttributeAsBoolean( "isReadable", readMethod != null ) && readMethod != null;
        
            // defaults to true if there is a write method, otherwise defaults to false
            isWriteable = attributeConfig.getAttributeAsBoolean( "isWriteable", writeMethod != null ) && writeMethod != null;
        }
        
        final boolean isIs = (readMethod != null) && readMethod.getName().startsWith("is");

        final ModelMBeanAttributeInfo info = new ModelMBeanAttributeInfo( name, type, description, isReadable, isWriteable, isIs );

        // additional info needed for modelMbean to work
        Descriptor descriptor = info.getDescriptor();
        descriptor.setField( "currencyTimeLimit" , new Integer(1) );
        if (isReadable) 
        {
            descriptor.setField( "getMethod" , readMethod.getName() );
        }
        if (writeMethod!=null) 
        {
            descriptor.setField( "setMethod" , writeMethod.getName() );
        }
        info.setDescriptor( descriptor );
        
        return info;
    }        

    /**
     *  Returns the PropertyDescriptor with the specified name from the array
     */
    private PropertyDescriptor getPropertyDescriptor( 
            String name, 
            PropertyDescriptor[] propertyDescriptorList )
        throws ConfigurationException 
    {
        
        for (int i = 0; i < propertyDescriptorList.length; i++) 
        {
            if ( propertyDescriptorList[i].getName().equals(name) )
            {
                return propertyDescriptorList[i];
            }
        }
        throw new ConfigurationException( 
            REZ.getString( "mxinfo.error.missing.property", name ) );
    }
    
    /**
     * Builds the management operations
     *
     * @param topicConfig topic configuration element to build from
     * @param beanInfo BeanInfo for managed class from introspector
     * @throws ConfigurationException  
     */    
    private ModelMBeanOperationInfo[] buildOperationInfos( 
            final Configuration topicConfig, final BeanInfo beanInfo ) 
       throws ConfigurationException
    {
        final Configuration[] operationsConfig = topicConfig.getChildren( "operation" );

        ModelMBeanOperationInfo[] operationList = 
            new ModelMBeanOperationInfo[operationsConfig.length];
        
        final MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
        
        for( int i = 0; i < operationsConfig.length; i++ )
        {
            operationList[i] = buildOperationInfo(
                getMethodDescriptor( 
                    operationsConfig[ i ].getAttribute("name" ), 
                    methodDescriptors ), 
                operationsConfig[ i ] );
        }
        
        return operationList;
    }

    /**
     * Builds an operation descriptor from a configuration node
     *
     * @param methodDescriptor methodDescriptor as returned from beanInfo
     * @param operation configuration element, can be null
     * @throws ConfigurationException if the configiration has the wrong elements
     * @return  the operation descriptor based on the configuration
     */    
    private ModelMBeanOperationInfo buildOperationInfo( 
            MethodDescriptor methodDescriptor,
            Configuration operationConfig ) 
       throws ConfigurationException
    {
        ModelMBeanOperationInfo info;
        
        if (operationConfig == null)
        {
            info = new ModelMBeanOperationInfo( methodDescriptor.getDisplayName(), 
                                                methodDescriptor.getMethod() );         

        }
        else 
        {
            final String name = methodDescriptor.getName();
            final String type = methodDescriptor.getMethod().getReturnType().getName();
            final String description = operationConfig.getAttribute( "description", 
                methodDescriptor.getDisplayName() );

            final int impact = 
                operationConfig.getAttributeAsInteger( "impact", ModelMBeanOperationInfo.UNKNOWN );

            final Configuration[] paramsConfig = operationConfig.getChildren( "param" );

            MBeanParameterInfo[] paramsList = 
                new MBeanParameterInfo[paramsConfig.length];

            for( int i = 0; i < paramsConfig.length; i++ )
            {
                paramsList[i] = buildParameterInfo( paramsConfig[ i ] );
            }

            info = new ModelMBeanOperationInfo( name, description, paramsList, type, impact );
        }
        
        // additional info needed for modelMbean to work
        Descriptor descriptor = info.getDescriptor();
        descriptor.setField( "currencyTimeLimit" , new Integer(1) );
        info.setDescriptor( descriptor );
        return info;    
    }        

    /**
     *  Returns the MethodDescriptor with the specified name from the array
     */
    private MethodDescriptor getMethodDescriptor( 
            String name, 
            MethodDescriptor[] methodDescriptorList )
        throws ConfigurationException 
    {
        
        for (int i = 0; i < methodDescriptorList.length; i++) 
        {
            if ( methodDescriptorList[i].getName().equals(name) )
            {
                return methodDescriptorList[i];
            }
        }
        throw new ConfigurationException( 
            REZ.getString( "mxinfo.error.missing.method", name ) );
    }
    
    /**
     * Builds the param descriptor from the configuration data
     *
     * @param param
     * @throws ConfigurationException if configuration not structured corretly
     * @return the descriptor
     */    
    private MBeanParameterInfo buildParameterInfo( Configuration paramConfig ) 
       throws ConfigurationException
    {
        final String name = paramConfig.getAttribute( "name" );
        final String description = paramConfig.getAttribute( "description" );
        final String type = paramConfig.getAttribute( "type" );
        
        return new MBeanParameterInfo( name, type, description );
    }        
    
    /**
     * Returns the configuration for the class or null if there is no mxinfo
     * file for it.
     *
     * @param clazz the class to load the configuration for
     * @throws ConfigurationException
     * @return the configuration file, or null if none exists
     */
    private Configuration loadMxInfo( Class clazz ) 
        throws ConfigurationException
    {
        String mxinfoName = "/" + clazz.getName().replace( '.', '/' ) + ".mxinfo";

        try 
        {

            InputStream stream = clazz.getResourceAsStream( mxinfoName );

            if (stream==null) 
            {
                return null;
            }

            InputSource source = new InputSource( stream );

            // build with validation against DTD
            return ConfigurationBuilder.build( source, true );
        }
        catch (Exception e) 
        {         
            final String message =
                REZ.getString( "mxinfo.error.file", mxinfoName );
            getLogger().error( message, e );
            throw new ConfigurationException( message );
        }
    }        
 
    /**
     *  Returns the class name without the package name
     */
    private String getShortName( String className ) 
    {
        return className.substring( className.lastIndexOf( '.' ) + 1 );
    }
}
