/*
 * Distributed as part of c3p0 v.0.9.1.2
 *
 * Copyright (C) 2005 Machinery For Change, Inc.
 *
 * Author: Steve Waldman <swaldman@mchange.com>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 2.1, as 
 * published by the Free Software Foundation.
 *
 * This software 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 software; see the file LICENSE.  If not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 */


package com.mchange.v2.c3p0.management;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.*;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;

import com.mchange.v1.lang.ClassUtils;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.mchange.v2.c3p0.DriverManagerDataSource;
import com.mchange.v2.c3p0.PooledDataSource;
import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource;
import com.mchange.v2.log.MLog;
import com.mchange.v2.log.MLogger;
import com.mchange.v2.log.MLevel;
import com.mchange.v2.management.ManagementUtils;

public class DynamicPooledDataSourceManagerMBean implements DynamicMBean
{
    final static MLogger logger = MLog.getLogger( DynamicPooledDataSourceManagerMBean.class );

    final static Set HIDE_PROPS;
    final static Set HIDE_OPS;
    final static Set FORCE_OPS;
    
    final static Set FORCE_READ_ONLY_PROPS;

    static
    {
        Set hpTmp = new HashSet();
        hpTmp.add("connectionPoolDataSource");
        hpTmp.add("nestedDataSource");
        hpTmp.add("reference");
        hpTmp.add("connection");
        hpTmp.add("password");
        hpTmp.add("pooledConnection");
        hpTmp.add("properties");
        hpTmp.add("logWriter");
        hpTmp.add("lastAcquisitionFailureDefaultUser");
        hpTmp.add("lastCheckoutFailureDefaultUser");
        hpTmp.add("lastCheckinFailureDefaultUser");
        hpTmp.add("lastIdleTestFailureDefaultUser");
        hpTmp.add("lastConnectionTestFailureDefaultUser");
        HIDE_PROPS = Collections.unmodifiableSet( hpTmp );
        
	Class[] userPassArgs = new Class[] { String.class, String.class };
        Set hoTmp = new HashSet();
        try
        {
            hoTmp.add(PooledDataSource.class.getMethod("close", new Class[] { boolean.class }) );
            hoTmp.add(PooledDataSource.class.getMethod("getConnection", userPassArgs ) );

            hoTmp.add(PooledDataSource.class.getMethod("getLastAcquisitionFailure", userPassArgs ) );
            hoTmp.add(PooledDataSource.class.getMethod("getLastCheckinFailure", userPassArgs ) );
            hoTmp.add(PooledDataSource.class.getMethod("getLastCheckoutFailure", userPassArgs ) );
            hoTmp.add(PooledDataSource.class.getMethod("getLastIdleTestFailure", userPassArgs ) );
            hoTmp.add(PooledDataSource.class.getMethod("getLastConnectionTestFailure", userPassArgs ) );
        }
        catch (Exception e)
        {
            logger.log(MLevel.WARNING, "Tried to hide an operation from being exposed by mbean, but failed to find the operation!", e);
        }
        HIDE_OPS = Collections.unmodifiableSet(hoTmp);
        
        Set fropTmp = new HashSet();
        fropTmp.add("identityToken");
        FORCE_READ_ONLY_PROPS = Collections.unmodifiableSet(fropTmp);

	Set foTmp = new HashSet();
	FORCE_OPS = Collections.unmodifiableSet(foTmp);
    }

    final static MBeanOperationInfo[] OP_INFS = extractOpInfs();

    MBeanInfo info = null;

    PooledDataSource pds;
    String mbeanName;
    MBeanServer mbs;
    
    ConnectionPoolDataSource cpds;
    DataSource unpooledDataSource;

    //attr names to attr infos
    Map pdsAttrInfos;               
    Map cpdsAttrInfos;              
    Map unpooledDataSourceAttrInfos;
    
    PropertyChangeListener pcl = new PropertyChangeListener()
    {
        public void propertyChange(PropertyChangeEvent evt)
        {
            String propName = evt.getPropertyName();
            Object val = evt.getNewValue();

            if ("nestedDataSource".equals(propName) || "connectionPoolDataSource".equals(propName))
                reinitialize();
        }
    };

    public DynamicPooledDataSourceManagerMBean(PooledDataSource pds, String mbeanName, MBeanServer mbs)
        throws Exception
    { 
        this.pds = pds; 
        this.mbeanName = mbeanName;
        this.mbs = mbs;
        
        if (pds instanceof ComboPooledDataSource)
            /* do nothing */;
        else if (pds instanceof AbstractPoolBackedDataSource)
            ((AbstractPoolBackedDataSource) pds).addPropertyChangeListener(pcl);
        else
            logger.warning(this + "managing an unexpected PooledDataSource. Only top-level attributes will be available. PooledDataSource: " + pds);
        
        Exception e = reinitialize();
        if (e != null) 
            throw e;
    }
    
    private synchronized Exception reinitialize()
    {
        try
        {
            // for ComboPooledDataSource, everything we care about is exposed via the PooledDataSource
            // for other implementations, we have to pay attention to nested DataSources
            if (!(pds instanceof ComboPooledDataSource) && pds instanceof AbstractPoolBackedDataSource)
            {
                if (this.cpds instanceof WrapperConnectionPoolDataSource) //implies non-null, this is a reinit
                    ((WrapperConnectionPoolDataSource) this.cpds).removePropertyChangeListener(pcl);
                
                
                // yeah, we reassign instantly, but for my comfort...
                this.cpds = null;
                this.unpooledDataSource = null;
                
                this.cpds = ((AbstractPoolBackedDataSource) pds).getConnectionPoolDataSource();

                if (cpds instanceof WrapperConnectionPoolDataSource)
                {
                    this.unpooledDataSource = ((WrapperConnectionPoolDataSource) cpds).getNestedDataSource();
                    ((WrapperConnectionPoolDataSource) this.cpds).addPropertyChangeListener(pcl);
                }
            }

            pdsAttrInfos = extractAttributeInfos( pds );
            cpdsAttrInfos = extractAttributeInfos( cpds );
            unpooledDataSourceAttrInfos = extractAttributeInfos( unpooledDataSource );

            Set allAttrNames = new HashSet();
            allAttrNames.addAll(pdsAttrInfos.keySet());
            allAttrNames.addAll(cpdsAttrInfos.keySet());
            allAttrNames.addAll(unpooledDataSourceAttrInfos.keySet());

            Set allAttrs = new HashSet();
            for(Iterator ii = allAttrNames.iterator(); ii.hasNext();)
            {
                String name = (String) ii.next();
                Object attrInfo;
                attrInfo = pdsAttrInfos.get(name);
                if (attrInfo == null)
                    attrInfo = cpdsAttrInfos.get(name);
                if (attrInfo == null)
                    attrInfo = unpooledDataSourceAttrInfos.get(name);
                allAttrs.add(attrInfo);
            }

            String className = this.getClass().getName();
            MBeanAttributeInfo[] attrInfos = (MBeanAttributeInfo[]) allAttrs.toArray(new MBeanAttributeInfo[ allAttrs.size() ]);
            Class[] ctorArgClasses = {PooledDataSource.class, String.class, MBeanServer.class};
            MBeanConstructorInfo[] constrInfos 
               = new MBeanConstructorInfo[] { new MBeanConstructorInfo("Constructor from PooledDataSource", this.getClass().getConstructor(ctorArgClasses)) };
            this.info = new MBeanInfo( this.getClass().getName(),
                            "An MBean to monitor and manage a PooledDataSource",
                            attrInfos,
                            constrInfos,
                            OP_INFS,
                            null);
            
            // we need to reregister when the attributes we support may have changed, to be sure
            // that the MBeanInfo is reread.
            try
            {
                ObjectName oname = ObjectName.getInstance( mbeanName );
                if (mbs.isRegistered( oname ))
                {
                    mbs.unregisterMBean( oname );
                    if (logger.isLoggable(MLevel.FINER))
                        logger.log(MLevel.FINER, "MBean: " + mbeanName + " unregistered, in order to be reregistered after update.");
                }
                mbs.registerMBean( this, oname );
                if (logger.isLoggable(MLevel.FINER))
                    logger.log(MLevel.FINER, "MBean: " + mbeanName + " registered.");
                
                return null;
            }
            catch (Exception e)
            {
                if ( logger.isLoggable(MLevel.WARNING) )
                    logger.log(MLevel.WARNING, 
                               "An Exception occurred while registering/reregistering mbean " + mbeanName +
                               ". MBean may not be registered, or may not work properly.",
                               e );
                return e;
            }
        }
        catch (NoSuchMethodException e)
        {
            if (logger.isLoggable(MLevel.SEVERE))
                logger.log( MLevel.SEVERE,
                                "Huh? We can't find our own constructor?? The one we're in?",
                                e);
            return e;
        }
    }

    // this method is fragile, makes assumptions that may have to change with
    // the PooledDataSource interface. It presumes that methods that look like
    // JavaBean properties should be skipped as attributes, that methods with
    // two string arguments are always username and password, that methods with
    // a return value are simple getters, while void methods are modifiers. At the
    // time of this writing, these assumptions all hold for PooledDataSource.
    // But beware the future.
    private static MBeanOperationInfo[] extractOpInfs()
    {
        MBeanParameterInfo user = new MBeanParameterInfo("user", "java.lang.String", "The database username of a pool-owner.");
        MBeanParameterInfo pwd = new MBeanParameterInfo("password", "java.lang.String", "The database password of a pool-owner.");
        MBeanParameterInfo[] userPass = {user, pwd};
        MBeanParameterInfo[] empty = {};

        Method[] meths = PooledDataSource.class.getMethods();
        Set attrInfos = new TreeSet(ManagementUtils.OP_INFO_COMPARATOR);

        for (int i = 0; i < meths.length; ++i)
        {
            Method meth = meths[i];
            if (HIDE_OPS.contains(meth))
                continue;
            
            String mname = meth.getName();
            Class[] params = meth.getParameterTypes();

	    if (! FORCE_OPS.contains(mname))
		{
		    //get rid of things we'd have picked up as attributes
		    if (mname.startsWith("set") && params.length == 1)
			continue;
		    if ((mname.startsWith("get") || mname.startsWith("is")) && params.length == 0)
			continue;
		}

            Class retType = meth.getReturnType();
            int impact = (retType == void.class ? MBeanOperationInfo.ACTION : MBeanOperationInfo.INFO);
            MBeanParameterInfo[] pi;
            if (params.length == 2 && params[0] == String.class && params[1] == String.class)
                pi = userPass;
            else if (params.length == 0)
                pi = empty;
            else
                pi = null;

            MBeanOperationInfo opi;
            if (pi != null)
                opi = new MBeanOperationInfo( mname, // name
                                null,  // desc
                                pi,
                                retType.getName(),
                                impact );
            else
            {
                //System.err.println("autobuilding opi from meth " + meth);
                opi = new MBeanOperationInfo(meth.toString(), meth);
            }

            //System.err.println("Created MBeanOperationInfo: " + opi + " [" + opi.getName() + ']');
            attrInfos.add( opi );
        }

        return (MBeanOperationInfo[]) attrInfos.toArray( new MBeanOperationInfo[ attrInfos.size() ] );
    }

    public synchronized Object getAttribute(String attr) throws AttributeNotFoundException, MBeanException, ReflectionException
    {
        try
        {
            AttrRec rec = attrRecForAttribute(attr);
            if (rec == null)
                throw new AttributeNotFoundException(attr);
            else
            {
                MBeanAttributeInfo ai = rec.attrInfo;
                if (! ai.isReadable() )
                    throw new IllegalArgumentException(attr + " not readable.");
                else
                {
                    String name = ai.getName();
                    String pfx = ai.isIs() ? "is" : "get";
                    String mname = pfx + Character.toUpperCase(name.charAt(0)) + name.substring(1);
                    Object target = rec.target; 
                    Method m = target.getClass().getMethod(mname, null);
                    return m.invoke(target, null);
                }
            }
        }
        catch (Exception e)
        {
            if (logger.isLoggable(MLevel.WARNING))
                logger.log(MLevel.WARNING, "Failed to get requested attribute: " + attr, e);
            throw new MBeanException(e);
        }
    }

    public synchronized AttributeList getAttributes(String[] attrs)
    {
        AttributeList al = new AttributeList();
        for (int i = 0, len = attrs.length; i < len; ++i)
        {
            String attr = attrs[i];
            try
            {
                Object val = getAttribute(attr);
                al.add(new Attribute(attr, val));
            }
            catch (Exception e)
            {
                if (logger.isLoggable(MLevel.WARNING))
                    logger.log(MLevel.WARNING, "Failed to get requested attribute (for list): " + attr, e);
            }
        }
        return al;
    }

    private AttrRec attrRecForAttribute(String attr)
    {
        assert (Thread.holdsLock(this));
        
        if (pdsAttrInfos.containsKey(attr))
            return new AttrRec(pds, (MBeanAttributeInfo) pdsAttrInfos.get(attr));
        else if (cpdsAttrInfos.containsKey(attr))
            return new AttrRec(cpds, (MBeanAttributeInfo) cpdsAttrInfos.get(attr));
        else if (unpooledDataSourceAttrInfos.containsKey(attr))
            return new AttrRec(unpooledDataSource, (MBeanAttributeInfo) unpooledDataSourceAttrInfos.get(attr));
        else
            return null;
    }

    public synchronized MBeanInfo getMBeanInfo()
    { 
        if (info == null)
            reinitialize();
        return info; 
    }

    public synchronized Object invoke(String operation, Object[] paramVals, String[] signature) throws MBeanException, ReflectionException
    {
        try
        {
            int slen = signature.length;
            Class[] paramTypes = new Class[ slen ];
            for (int i = 0; i < slen; ++i)
                paramTypes[i] = ClassUtils.forName( signature[i] );
            
            //all operations should be on pds
            Method m = pds.getClass().getMethod(operation, paramTypes);
            return m.invoke(pds, paramVals);
        }
        catch (NoSuchMethodException e)
        {
            // although not generally legal as of the latest JMX spec,
            // someone could be trying to work with attributes through
            // invoke. If so, we try to deal
            try
            {
            boolean two = false;
            if (signature.length == 0 && ( operation.startsWith("get") || (two = operation.startsWith("is")) ))
            {
                int i = two ? 2 : 3;
                String attr = Character.toLowerCase(operation.charAt(i)) + operation.substring(i + 1);
                return getAttribute( attr );
            }
            else if (signature.length == 1 && operation.startsWith("set"))
            {
                setAttribute(new Attribute(Character.toLowerCase(operation.charAt(3)) + operation.substring(4), paramVals[0]));
                return null;
            }
            else
                throw new MBeanException(e);
            }
            catch (Exception e2)
            { throw new MBeanException(e2); }
        }
        catch (Exception e)
        { throw new MBeanException(e); }
    }

    public synchronized void setAttribute(Attribute attrObj) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException
    {
        try
        {
            String attr = attrObj.getName();
            
            if (attr == "factoryClassLocation") // special case
            {
                if (pds instanceof ComboPooledDataSource)
                {
                    ((ComboPooledDataSource) pds).setFactoryClassLocation((String) attrObj.getValue());
                    return;
                }
                else if (pds instanceof AbstractPoolBackedDataSource)
                {
                    String val = (String) attrObj.getValue();
                    AbstractPoolBackedDataSource apbds = ((AbstractPoolBackedDataSource) pds);
                    apbds.setFactoryClassLocation( val );
                    ConnectionPoolDataSource checkDs1 = apbds.getConnectionPoolDataSource();
                    if (checkDs1 instanceof WrapperConnectionPoolDataSource)
                    {
                        WrapperConnectionPoolDataSource wcheckDs1 = (WrapperConnectionPoolDataSource) checkDs1;
                        wcheckDs1.setFactoryClassLocation( val );
                        DataSource checkDs2 = wcheckDs1.getNestedDataSource();
                        if (checkDs2 instanceof DriverManagerDataSource)
                            ((DriverManagerDataSource) checkDs2).setFactoryClassLocation( val );
                    }
                    return;
                }
                // else try treating factoryClassLocation like any other attribute
                // on the presumption that some future, unexpected DataSource that
                // exposes this property will not require the property to be set at
                // multiple levels, as PoolBackedDataSource does...
            }
            
            AttrRec rec = attrRecForAttribute(attr);
            if (rec == null)
                throw new AttributeNotFoundException(attr);
            else
            {
                MBeanAttributeInfo ai = rec.attrInfo;
                if (! ai.isWritable() )
                    throw new IllegalArgumentException(attr + " not writable.");
                else
                {
                    Class attrType = ClassUtils.forName( rec.attrInfo.getType() );
                    String name = ai.getName();
                    String pfx = "set";
                    String mname = pfx + Character.toUpperCase(name.charAt(0)) + name.substring(1);
                    Object target = rec.target; 
                    Method m = target.getClass().getMethod(mname, new Class[] {attrType});
                    m.invoke(target, new Object[] { attrObj.getValue() });
                    
                    // if we were unable to set this attribute directly in the PooledDataSource,
                    // we are updating a property of a nested DataSource, and we should reset
                    // the pool manager of the PooledDataSource implementation so that these
                    // properties are reread and the changes take effect.
                    if (target != pds)
                    {
                         if (pds instanceof AbstractPoolBackedDataSource)
                            ((AbstractPoolBackedDataSource) pds).resetPoolManager(false);
                         else if (logger.isLoggable(MLevel.WARNING))
                             logger.warning("MBean set a nested ConnectionPoolDataSource or DataSource parameter on an unknown PooledDataSource type. " + 
                                             "Could not reset the pool manager, so the changes may not take effect. " + "" +
                                              "c3p0 may need to be updated for PooledDataSource type " + pds.getClass() + ".");
                             
                    }
                }
            }
        }
        catch (Exception e)
        {
            if (logger.isLoggable(MLevel.WARNING))
                logger.log(MLevel.WARNING, "Failed to set requested attribute: " + attrObj, e);
            throw new MBeanException(e);
        }
    }

    public synchronized AttributeList setAttributes(AttributeList al)
    {
        AttributeList out = new AttributeList();
        for (int i = 0, len = al.size(); i < len; ++i)
        {
            Attribute attrObj = (Attribute) al.get(i);
            
            try
            {
                this.setAttribute( attrObj );
                out.add(attrObj);
            }
            catch (Exception e)
            {
                if (logger.isLoggable(MLevel.WARNING))
                    logger.log(MLevel.WARNING, "Failed to set requested attribute (from list): " + attrObj, e);
            }
        }
        return out;
    }

    private static Map extractAttributeInfos(Object bean)
    {
        if ( bean != null)
        {
            try
            {
                Map out = new HashMap();
                BeanInfo beanInfo = Introspector.getBeanInfo( bean.getClass(), Object.class ); //so we don't see getClass() as a property
                PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
                //System.err.println("ignoreProps: " + ignoreProps );
                for( int i = 0, len = pds.length; i < len; ++i)
                {
                    PropertyDescriptor pd = pds[i];

                    String name;
                    String desc;
                    Method getter;
                    Method setter;

                    name = pd.getName();

                    if (HIDE_PROPS.contains( name ))
                        continue;

                    desc = getDescription( name );
                    getter = pd.getReadMethod();
                    setter = pd.getWriteMethod();
                    
                    if (FORCE_READ_ONLY_PROPS.contains(name))
                        setter = null;

                    /*
                     * Note that it's not a problem that these
                     * getters and setters are not against this class
                     * the MBeanAttributInfo just uses the method
                     * names and attribute type to construct itself,
                     * and does not hold the methods themselves for
                     * future invocation.
                     */

                    try
                    {
                        out.put( name, new MBeanAttributeInfo(name, desc, getter, setter) );
                    }
                    catch (javax.management.IntrospectionException e)
                    {
                        if (logger.isLoggable( MLevel.WARNING ))
                            logger.log( MLevel.WARNING, "IntrospectionException while setting up MBean attribute '" + name + "'", e);
                    }
                }

                return Collections.synchronizedMap(out);
            }
            catch (java.beans.IntrospectionException e)
            {
                if (logger.isLoggable( MLevel.WARNING ))
                    logger.log( MLevel.WARNING, "IntrospectionException while setting up MBean attributes for " + bean, e);
                return Collections.EMPTY_MAP;
            }
        }
        else
            return Collections.EMPTY_MAP;
    }

    // TODO: use a ResourceBundle for attribute descriptions.
    // (Extra credit -- build from xml file, build docs same way)
    private static String getDescription(String attrName)
    { return null; }

    private static class AttrRec
    {
        Object target;
        MBeanAttributeInfo attrInfo;
    
        AttrRec(Object target, MBeanAttributeInfo attrInfo)
        {
            this.target = target;
            this.attrInfo = attrInfo;
        }
    }

}
