/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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.1 of
 * the License, or (at your option) any later version.
 *
 * 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; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.ant;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import javax.management.Attribute;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.jboss.util.propertyeditor.PropertyEditors;

/**
 * JMX.java. An ant plugin to call managed operations and set attributes
 * on mbeans in a jboss jmx mbean server.
 * To use this plugin with Ant, place the jbossjmx-ant.jar together with the
 * jboss jars jboss-j2ee.jar and jboss-common-client.jar, and the sun jnet.jar in the
 * ant/lib directory you wish to use.
 * If the JMX invoker is secured, set the username and password attributes in the task.
 *
 * Here is an example from an ant build file.
 *
 * <target name="jmx">
 *   <taskdef name="jmx"
 *	classname="org.jboss.ant.JMX"/>
 *   <jmx adapterName="jmx:HP.home.home:rmi">
 *
 *     <propertyEditor type="java.math.BigDecimal" editor="org.jboss.util.propertyeditor.BigDecimalEditor"/>
 *     <propertyEditor type="java.util.Date" editor="org.jboss.util.propertyeditor.DateEditor"/>
 *
 *
 *      <!-- define classes -->
 *     <invoke target="fgm.sysadmin:service=DefineClasses"
 *             operation="defineClasses">
 *       <parameter type="java.lang.String" arg="defineclasses.xml"/>
 *     </invoke>
 *   </jmx>
 *
 *
 * Created: Tue Jun 11 20:17:44 2002
 *
 * @author <a href="mailto:d_jencks@users.sourceforge.net">David Jencks</a>
 * @author <a href="mailto:dsnyder_lion@users.sourceforge.net">David Snyder</a>
 * @version
 */
public class JMX extends Task
{
   private String serverURL;

   private String adapterName = "jmx/invoker/RMIAdaptor";

   private String username;

   private String password;

   private List<Operation> ops = new ArrayList<Operation>();

   private List<PropertyEditorHolder> editors = new ArrayList<PropertyEditorHolder>();

   /**
    * Creates a new <code>JMX</code> instance.
    * Provides a default adapterName for the current server, so you only need to set it to
    * talk to a remote server.
    *
    * @exception Exception if an error occurs
    */
   public JMX() throws Exception
   {
   }

   /**
    * Use the <code>setServerURL</code> method to set the URL of the server
    * you wish to connect to.
    *
    * @param serverURL a <code>String</code> value
    */
   public void setServerURL(String serverURL)
   {
      this.serverURL = serverURL;
   }

   /**
    * Use the <code>setAdapterName</code> method to set the name the
    * adapter mbean is bound under in jndi.
    *
    * @param adapterName a <code>String</code> value
    */
   public void setAdapterName(String adapterName)
   {
      this.adapterName = adapterName;
   }

   /**
    * Use the <code>setUsername</code> method to set the username for 
    * the JMX invoker (if it's secured).
    * 
    * @param username a <code>String</code> value
    */
   public void setUsername(String username)
   {
      this.username = username;
   }

   /**
    * Use the <code>setPassword</code> method to set the password for 
    * the JMX invoker (if it's secured).
    * 
    * @param password a <code>String</code> value
    */
   public void setPassword(String password)
   {
      this.password = password;
   }

   /**
    * Use the <code>addInvoke</code> method to add an <invoke> operation.
    * Include as attributes the target ObjectName and operation name.
    * Include as sub-elements parameters: see addParameter in the Invoke class.
    *
    * @param invoke an <code>Invoke</code> value
    */
   public void addInvoke(Invoke invoke)
   {
      ops.add(invoke);
   }

   /**
    * Use the  <code>addSetAttribute</code> method to add a set-attribute
    * operation. Include as attributes the target ObjectName and the
    * the attribute name.  Include the value as a nested value tag
    * following the parameter syntax.
    *
    * @param setter a <code>Setter</code> value
    */
   public void addSetAttribute(Setter setter)
   {
      ops.add(setter);
   }

   /**
    * Use the  <code>addGetAttribute</code> method to add a get-attribute
    * operation. Include as attributes the target ObjectName, the
    * the attribute name, and a property name to hold the result of the
    * get-attribute operation.
    *
    * @param getter a <code>Getter</code> value
    */
   public void addGetAttribute(Getter getter)
   {
      ops.add(getter);
   }

   /**
    * Use the <code>addPropertyEditor</code> method to make a PropertyEditor
    * available for values.  Include attributes for the type and editor fully
    * qualified class name.
    *
    * @param peh a <code>PropertyEditorHolder</code> value
    */
   public void addPropertyEditor(PropertyEditorHolder peh)
   {
      editors.add(peh);
   }

   /**
    * The <code>execute</code> method is called by ant to execute the task.
    *
    * @exception BuildException if an error occurs
    */
   public void execute() throws BuildException
   {
      final ClassLoader origCL = Thread.currentThread().getContextClassLoader();
      try
      {
         Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
         try
         {
            for (int i = 0; i < editors.size(); i++)
            {
               editors.get(i).execute();
            } // end of for ()

         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new BuildException("Could not register property editors: " + e);
         } // end of try-catch

         try
         {
            Properties props = new Properties();
            String factory = "org.jnp.interfaces.NamingContextFactory";
            if (username != null && password != null)
            {
               factory = "org.jboss.security.jndi.JndiLoginInitialContextFactory";
               props.put(Context.SECURITY_PRINCIPAL, username);
               props.put(Context.SECURITY_CREDENTIALS, password);
            }
            props.put(Context.INITIAL_CONTEXT_FACTORY, factory);
            props.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");

            if (serverURL == null || "".equals(serverURL))
            {
               props.put(Context.PROVIDER_URL, "jnp://localhost:1099");
            }
            else
            {
               props.put(Context.PROVIDER_URL, serverURL);
            }
            InitialContext ctx = new InitialContext(props);

            // if adapter is null, the use the default
            if (adapterName == null)
            {
               adapterName = "jmx/rmi/RMIAdaptor";//org.jboss.jmx.adaptor.rmi.RMIAdaptorService.DEFAULT_JNDI_NAME;
            }

            Object obj = ctx.lookup(adapterName);
            ctx.close();

            if (!(obj instanceof MBeanServerConnection))
            {
               throw new ClassCastException("Object not of type: MBeanServerConnection, but: "
                     + (obj == null ? "not found" : obj.getClass().getName()));
            }

            MBeanServerConnection server = (MBeanServerConnection) obj;

            for (int i = 0; i < ops.size(); i++)
            {
               Operation op = ops.get(i);
               op.execute(server, this);
            } // end of for ()

         }
         catch (Exception e)
         {
            e.printStackTrace();
            throw new BuildException("problem: " + e);
         } // end of try-catch
      }
      finally
      {
         Thread.currentThread().setContextClassLoader(origCL);
      }

   }

   /**
    * The interface <code>Operation</code> provides a common interface
    * for the sub-tasks..
    *
    */
   public static interface Operation
   {
      void execute(MBeanServerConnection server, Task parent) throws Exception;
   }

   /**
    * The class <code>Invoke</code> specifies the invocation of a
    * managed operation.
    *
    */
   public static class Invoke implements Operation
   {
      private ObjectName target;

      private String property;

      private String operation;

      private List<Param> params = new ArrayList<Param>();

      /**
       * The <code>setProperty</code> method sets the name of the property
       * that will contain the result of the operation.
       *
       * @param property a <code>String</code> value
       */
      public void setProperty(String property)
      {
         this.property = property;
      }

      /**
       * The <code>setTarget</code> method sets the ObjectName
       * of the target mbean.
       *
       * @param target an <code>ObjectName</code> value
       */
      public void setTarget(ObjectName target)
      {
         this.target = target;
      }

      /**
       * The <code>setOperation</code> method specifies the operation to
       * be performed.
       *
       * @param operation a <code>String</code> value
       */
      public void setOperation(String operation)
      {
         this.operation = operation;
      }

      /**
       * The <code>addParameter</code> method adds a parameter for
       * the operation. You must specify type and value.
       *
       * @param param a <code>Param</code> value
       */
      public void addParameter(Param param)
      {
         params.add(param);
      }

      public void execute(MBeanServerConnection server, Task parent) throws Exception
      {
         int paramCount = params.size();
         Object[] args = new Object[paramCount];
         String[] types = new String[paramCount];
         int pos = 0;
         for (int i = 0; i < params.size(); i++)
         {
            Param p = params.get(i);
            args[pos] = p.getValue();
            types[pos] = p.getType();
            pos++;
         } // end of for ()
         Object result = server.invoke(target, operation, args, types);
         if ((property != null) && (result != null))
         {
            parent.getProject().setProperty(property, result.toString());
         }
      }
   }

   /**
    * The class <code>Setter</code> specifies setting an attribute
    * value on an mbean.
    *
    */
   public static class Setter implements Operation
   {
      private ObjectName target;

      private String attribute;

      private Param value;

      /**
       * The <code>setTarget</code> method sets the ObjectName
       * of the target mbean.
       *
       * @param target an <code>ObjectName</code> value
       */
      public void setTarget(ObjectName target)
      {
         this.target = target;
      }

      /**
       * The <code>setAttribute</code> method specifies the attribute to be set.
       *
       * @param attribute a <code>String</code> value
       */
      public void setAttribute(String attribute)
      {
         this.attribute = attribute;
      }

      /**
       * The <code>setValue</code> method specifies the value to be used.
       * The type is used to convert the value to the correct type.
       *
       * @param value a <code>Param</code> value
       */
      public void setValue(Param value)
      {
         this.value = value;
      }

      public void execute(MBeanServerConnection server, Task parent) throws Exception
      {
         Attribute att = new Attribute(attribute, value.getValue());
         server.setAttribute(target, att);
      }
   }

   /**
    * The class <code>Getter</code> specifies getting an attribute
    * value of an mbean.
    *
    */
   public static class Getter implements Operation
   {
      private ObjectName target;

      private String attribute;

      private String property;

      /**
       * The <code>setTarget</code> method sets the ObjectName
       * of the target mbean.
       *
       * @param target an <code>ObjectName</code> value
       */
      public void setTarget(ObjectName target)
      {
         this.target = target;
      }

      /**
       * The <code>setAttribute</code> method specifies the attribute to be
       * retrieved.
       *
       * @param attribute a <code>String</code> value
       */
      public void setAttribute(String attribute)
      {
         this.attribute = attribute;
      }

      /**
       * The <code>setProperty</code> method specifies the name of the property
       * to be set with the attribute value.
       *
       * @param property a <code>String</code> value
       */
      public void setProperty(String property)
      {
         this.property = property;
      }

      public void execute(MBeanServerConnection server, Task parent) throws Exception
      {
         Object result = server.getAttribute(target, attribute);
         if ((property != null) && (result != null))
         {
            parent.getProject().setProperty(property, result.toString());
         }
      }
   }

   /**
    * The class <code>Param</code> is used to represent a object by
    * means of a string representation of its value and its type.
    *
    */
   public static class Param
   {
      private String arg;

      private String type;

      /**
       * The <code>setArg</code> method sets the string representation
       * of the parameters value.
       *
       * @param arg a <code>String</code> value
       */
      public void setArg(String arg)
      {
         this.arg = arg;
      }

      public String getArg()
      {
         return arg;
      }

      /**
       * The <code>setType</code> method sets the fully qualified class
       * name of the type represented by the param object.
       *
       * @param type a <code>String</code> value
       */
      public void setType(String type)
      {
         this.type = type;
      }

      public String getType()
      {
         return type;
      }

      /**
       * The <code>getValue</code> method uses PropertyEditors to convert
       * the string representation of the value to an object, which it returns.
       * The PropertyEditor to use is determined by the type specified.
       *
       * @return an <code>Object</code> value
       * @exception Exception if an error occurs
       */
      public Object getValue() throws Exception
      {
         PropertyEditor editor = PropertyEditors.getEditor(type);
         editor.setAsText(arg);
         return editor.getValue();
      }
   }

   /**
    * The class <code>PropertyEditorHolder</code> allows you to add a
    * PropertyEditor to the default set.
    *
    */
   public static class PropertyEditorHolder
   {
      private String type;

      private String editor;

      /**
       * The <code>setType</code> method specifies the return type from the
       * property editor.
       *
       * @param type a <code>String</code> value
       */
      public void setType(final String type)
      {
         this.type = type;
      }

      public String getType()
      {
         return type;
      }

      private Class<?> getTypeClass() throws ClassNotFoundException
      {
         //with a little luck, one of these will work with Ant's classloaders
         try
         {
            return Class.forName(type);
         }
         catch (ClassNotFoundException e)
         {
         } // end of try-catch
         try
         {
            return getClass().getClassLoader().loadClass(type);
         }
         catch (ClassNotFoundException e)
         {
         } // end of try-catch
         return Thread.currentThread().getContextClassLoader().loadClass(type);
      }

      /**
       * The <code>setEditor</code> method specifies the fully qualified
       * class name of the PropertyEditor for the type specified in the type field.
       *
       * @param editor a <code>String</code> value
       */
      public void setEditor(final String editor)
      {
         this.editor = editor;
      }

      public String getEditor()
      {
         return editor;
      }

      private Class<?> getEditorClass() throws ClassNotFoundException
      {
         //with a little luck, one of these will work with Ant's classloaders
         try
         {
            return Class.forName(editor);
         }
         catch (ClassNotFoundException e)
         {
         } // end of try-catch
         try
         {
            return getClass().getClassLoader().loadClass(editor);
         }
         catch (ClassNotFoundException e)
         {
         } // end of try-catch
         return Thread.currentThread().getContextClassLoader().loadClass(editor);
      }

      public void execute() throws ClassNotFoundException
      {
         PropertyEditorManager.registerEditor(getTypeClass(), getEditorClass());
      }
   }

}// JMX
