/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt 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.invocation.jrmp.server;

import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.Serializable;
import java.rmi.server.RemoteServer;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RemoteStub;
import java.rmi.MarshalledObject;
import java.security.PrivilegedAction;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;

import javax.management.ObjectName;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.naming.Name;
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.NameNotFoundException;
import javax.transaction.Transaction;

import org.jboss.invocation.jrmp.interfaces.JRMPInvokerProxy;
import org.jboss.invocation.Invocation;
import org.jboss.invocation.Invoker;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.invocation.MarshalledValueInputStream;
import org.jboss.logging.Logger;
import org.jboss.mx.util.JMXExceptionDecoder;
import org.jboss.net.sockets.DefaultSocketFactory;
import org.jboss.security.SecurityDomain;
import org.jboss.system.Registry;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.tm.TransactionPropagationContextImporter;
import org.jboss.tm.TransactionPropagationContextUtil;

/**
 * The JRMPInvoker is an RMI implementation that can generate Invocations
 * from RMI/JRMP into the JMX base.
 *
 * @author <a href="mailto:marc.fleury@jboss.org>Marc Fleury</a>
 * @author <a href="mailto:scott.stark@jboss.org>Scott Stark</a>
 * @version $Revision$
 * @jmx.mbean extends="org.jboss.system.ServiceMBean"
 */
public class JRMPInvoker
   extends RemoteServer
   implements Invoker, JRMPInvokerMBean, MBeanRegistration
{
   /**
    * Identifer to instruct the usage of an anonymous port.
    */
   public static final int ANONYMOUS_PORT = 0;

   /**
    * Instance logger.
    */
   protected Logger log;

   /**
    * Service MBean support delegate.
    */
   protected ServiceMBeanSupport support;

   /**
    * The port the container will be exported on
    */
   protected int rmiPort = ANONYMOUS_PORT;

   /**
    * An optional custom client socket factory
    */
   protected RMIClientSocketFactory clientSocketFactory;

   /**
    * An optional custom server socket factory
    */
   protected RMIServerSocketFactory serverSocketFactory;

   /**
    * The class name of the optional custom client socket factory
    */
   protected String clientSocketFactoryName;

   /**
    * The class name of the optional custom server socket factory
    */
   protected String serverSocketFactoryName;

   /**
    * The address to bind the rmi port on
    */
   protected String serverAddress;
   /**
    * The name of the security domain to use with server sockets that support SSL
    */
   protected String sslDomain;

   protected RemoteStub invokerStub;
   /**
    * The socket accept backlog
    */
   protected int backlog = 200;
   /**
    * A flag to enable caching of classes in the MarshalledValueInputStream
    */
   protected boolean enableClassCaching = false;
   /**
    * A priviledged actions for MBeanServer.invoke when running with sec mgr
    */
   private MBeanServerAction serverAction = new MBeanServerAction();

   private static TransactionPropagationContextImporter tpcImporter;

   public JRMPInvoker()
   {
      final JRMPInvoker delegate = this;

      // adapt the support delegate to invoke our state methods
      support = new ServiceMBeanSupport(getClass())
      {
         protected void startService() throws Exception
         {
            delegate.startService();
         }
         protected void stopService() throws Exception
         {
            delegate.stopService();
         }
         protected void destroyService() throws Exception
         {
            delegate.destroyService();
         }
      };

      // Setup logging from delegate
      log = support.getLog();
   }

   /**
    * @jmx.managed-attribute
    */
   public int getBacklog()
   {
      return backlog;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setBacklog(int back)
   {
      backlog = back;
   }

   /**
    * @jmx.managed-attribute
    */
   public boolean getEnableClassCaching()
   {
      return enableClassCaching;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setEnableClassCaching(boolean flag)
   {
      enableClassCaching = flag;
      MarshalledValueInputStream.useClassCache(enableClassCaching);
   }

   /**
    * @return The localhost name or null.
    */
   public String getServerHostName()
   {
      try
      {
         return InetAddress.getLocalHost().getHostName();
      }
      catch (Exception ignored)
      {
         return null;
      }
   }

   /**
    * @jmx.managed-attribute
    */
   public void setRMIObjectPort(final int rmiPort)
   {
      this.rmiPort = rmiPort;
   }

   /**
    * @jmx.managed-attribute
    */
   public int getRMIObjectPort()
   {
      return rmiPort;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setRMIClientSocketFactory(final String name)
   {
      clientSocketFactoryName = name;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getRMIClientSocketFactory()
   {
      return clientSocketFactoryName;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setRMIClientSocketFactoryBean(final RMIClientSocketFactory bean)
   {
      clientSocketFactory = bean;
   }

   /**
    * @jmx.managed-attribute
    */
   public RMIClientSocketFactory getRMIClientSocketFactoryBean()
   {
      return clientSocketFactory;
   }
   
   /**
    * @jmx.managed-attribute
    */
   public void setRMIServerSocketFactory(final String name)
   {
      serverSocketFactoryName = name;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getRMIServerSocketFactory()
   {
      return serverSocketFactoryName;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setRMIServerSocketFactoryBean(final RMIServerSocketFactory bean)
   {
      serverSocketFactory = bean;
   }

   /**
    * @jmx.managed-attribute
    */
   public RMIServerSocketFactory getRMIServerSocketFactoryBean()
   {
      return serverSocketFactory;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setServerAddress(final String address)
   {
      serverAddress = address;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getServerAddress()
   {
      return serverAddress;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setSecurityDomain(String domainName)
   {
      this.sslDomain = domainName;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getSecurityDomain()
   {
      return sslDomain;
   }

   public Serializable getStub()
   {
      return this.invokerStub;
   }

   protected void startService() throws Exception
   {
      loadCustomSocketFactories();

      if (log.isDebugEnabled())
      {
         log.debug("RMI Port='" +
            (rmiPort == ANONYMOUS_PORT ? "Anonymous" :
            Integer.toString(rmiPort)) + "'");

         log.debug("Client SocketFactory='" +
            (clientSocketFactory == null ? "Default" :
            clientSocketFactory.toString()) + "'");

         log.debug("Server SocketFactory='" +
            (serverSocketFactory == null ? "Default" :
            serverSocketFactory.toString()) + "'");

         log.debug("Server SocketAddr='" +
            (serverAddress == null ? "Default" :
            serverAddress) + "'");
         log.debug("SecurityDomain='" +
            (sslDomain == null ? "Default" :
            sslDomain) + "'");
      }

      InitialContext ctx = new InitialContext();

      // FIXME marcf: This should not be here
      // and the transaction propagation context importer
      tpcImporter = TransactionPropagationContextUtil.getTPCImporter();


      // Set the transaction manager and transaction propagation
      // context factory of the GenericProxy class

      Invoker delegateInvoker = createDelegateInvoker();

      // Make the remote invoker proxy available for use by the proxy factory
      Registry.bind(support.getServiceName(), delegateInvoker);

      // Export CI
      exportCI();

      log.debug("Bound JRMP invoker for JMX node");

      ctx.close();
   }

   protected void stopService() throws Exception
   {
      InitialContext ctx = new InitialContext();

      try
      {
         unexportCI();
      }
      finally
      {
         ctx.close();
      }
      this.clientSocketFactory = null;
      this.serverSocketFactory = null;
      this.invokerStub = null;
   }

   protected void destroyService() throws Exception
   {
      // Export references to the bean
      Registry.unbind(support.getServiceName());
   }

   /**
    * Invoke a Remote interface method.
    */
   public Object invoke(Invocation invocation)
      throws Exception
   {
      ClassLoader oldCl = TCLAction.UTIL.getContextClassLoader();
      ObjectName mbean = null;
      try
      {
         // Deserialize the transaction if it is there
         MarshalledInvocation mi = (MarshalledInvocation) invocation;
         invocation.setTransaction(importTPC(mi.getTransactionPropagationContext()));

         mbean = (ObjectName) Registry.lookup(invocation.getObjectName());

         // The cl on the thread should be set in another interceptor
         Object obj = serverAction.invoke(mbean,
            "invoke",
            new Object[]{invocation},
            Invocation.INVOKE_SIGNATURE);
         return new MarshalledObject(obj);
      }
      catch (Exception e)
      {
         Throwable th = JMXExceptionDecoder.decode(e);
         if (log.isTraceEnabled())
            log.trace("Failed to invoke on mbean: " + mbean, th);

         if (th instanceof Exception)
            e = (Exception) th;

         throw e;
      }
      finally
      {
         TCLAction.UTIL.setContextClassLoader(oldCl);
         Thread.interrupted(); // clear interruption because this thread may be pooled.
      }
   }

   protected Invoker createDelegateInvoker()
   {
      return new JRMPInvokerProxy(this);
   }

   protected void exportCI() throws Exception
   {
      this.invokerStub = (RemoteStub) UnicastRemoteObject.exportObject
         (this, rmiPort, clientSocketFactory, serverSocketFactory);
   }

   protected void unexportCI() throws Exception
   {
      UnicastRemoteObject.unexportObject(this, true);
   }

   protected void rebind(Context ctx, String name, Object val)
      throws NamingException
   {
      // Bind val to name in ctx, and make sure that all
      // intermediate contexts exist

      Name n = ctx.getNameParser("").parse(name);
      while (n.size() > 1)
      {
         String ctxName = n.get(0);
         try
         {
            ctx = (Context) ctx.lookup(ctxName);
         }
         catch (NameNotFoundException e)
         {
            ctx = ctx.createSubcontext(ctxName);
         }
         n = n.getSuffix(1);
      }

      ctx.rebind(n.get(0), val);
   }

   /**
    * Load and instantiate the clientSocketFactory, serverSocketFactory using
    * the TCL and set the bind address and SSL domain if the serverSocketFactory
    * supports it.
    */
   protected void loadCustomSocketFactories()
   {
      ClassLoader loader = TCLAction.UTIL.getContextClassLoader();

      if( clientSocketFactory == null )
      {
         try
         {
            if (clientSocketFactoryName != null)
            {
               Class csfClass = loader.loadClass(clientSocketFactoryName);
               clientSocketFactory = (RMIClientSocketFactory) csfClass.newInstance();
            }
         }
         catch (Exception e)
         {
            log.error("Failed to load client socket factory", e);
            clientSocketFactory = null;
         }
      }

      if( serverSocketFactory == null )
      {
         try
         {
            if (serverSocketFactoryName != null)
            {
               Class ssfClass = loader.loadClass(serverSocketFactoryName);
               serverSocketFactory = (RMIServerSocketFactory) ssfClass.newInstance();
               if (serverAddress != null)
               {
                  // See if the server socket supports setBindAddress(String)
                  try
                  {
                     Class[] parameterTypes = {String.class};
                     Method m = ssfClass.getMethod("setBindAddress", parameterTypes);
                     Object[] args = {serverAddress};
                     m.invoke(serverSocketFactory, args);
                  }
                  catch (NoSuchMethodException e)
                  {
                     log.warn("Socket factory does not support setBindAddress(String)");
                     // Go with default address
                  }
                  catch (Exception e)
                  {
                     log.warn("Failed to setBindAddress=" + serverAddress + " on socket factory", e);
                     // Go with default address
                  }
               }
               /* See if the server socket supports setSecurityDomain(SecurityDomain)
               if an sslDomain was specified
               */
               if (sslDomain != null)
               {
                  try
                  {
                     InitialContext ctx = new InitialContext();
                     SecurityDomain domain = (SecurityDomain) ctx.lookup(sslDomain);
                     Class[] parameterTypes = {SecurityDomain.class};
                     Method m = ssfClass.getMethod("setSecurityDomain", parameterTypes);
                     Object[] args = {domain};
                     m.invoke(serverSocketFactory, args);
                  }
                  catch (NoSuchMethodException e)
                  {
                     log.error("Socket factory does not support setSecurityDomain(SecurityDomain)");
                  }
                  catch (Exception e)
                  {
                     log.error("Failed to setSecurityDomain=" + sslDomain + " on socket factory", e);
                  }
               }
            }
            // If a bind address was specified create a DefaultSocketFactory
            else if (serverAddress != null)
            {
               DefaultSocketFactory defaultFactory = new DefaultSocketFactory(backlog);
               serverSocketFactory = defaultFactory;
               try
               {
                  defaultFactory.setBindAddress(serverAddress);
               }
               catch (UnknownHostException e)
               {
                  log.error("Failed to setBindAddress=" + serverAddress + " on socket factory", e);
               }
            }
         }
         catch (Exception e)
         {
            log.error("operation failed", e);
            serverSocketFactory = null;
         }
      }
   }

   /**
    * Import a transaction propagation context into the local VM, and
    * return the corresponding <code>Transaction</code>.
    *
    * @return A transaction or null if no tpc.
    */
   protected Transaction importTPC(Object tpc)
   {
      if (tpc != null)
         return tpcImporter.importTransactionPropagationContext(tpc);
      return null;
   }

   //
   // Delegate the ServiceMBean details to our support delegate
   //

   public String getName()
   {
      return support.getName();
   }

   public MBeanServer getServer()
   {
      return support.getServer();
   }

   public int getState()
   {
      return support.getState();
   }

   public String getStateString()
   {
      return support.getStateString();
   }

   public void create() throws Exception
   {
      support.create();
   }

   public void start() throws Exception
   {
      support.start();
   }

   public void stop()
   {
      support.stop();
   }

   public void destroy()
   {
      support.destroy();
   }

   public void jbossInternalLifecycle(String method) throws Exception
   {
      support.jbossInternalLifecycle(method);
   }

   public ObjectName preRegister(MBeanServer server, ObjectName name)
      throws Exception
   {
      return support.preRegister(server, name);
   }

   public void postRegister(Boolean registrationDone)
   {
      support.postRegister(registrationDone);
   }

   public void preDeregister() throws Exception
   {
      support.preDeregister();
   }

   public void postDeregister()
   {
      support.postDeregister();
   }

   interface TCLAction
   {
      class UTIL
      {
         static TCLAction getTCLAction()
         {
            return System.getSecurityManager() == null ? NON_PRIVILEGED : PRIVILEGED;
         }

         static ClassLoader getContextClassLoader()
         {
            return getTCLAction().getContextClassLoader();
         }

         static ClassLoader getContextClassLoader(Thread thread)
         {
            return getTCLAction().getContextClassLoader(thread);
         }

         static void setContextClassLoader(ClassLoader cl)
         {
            getTCLAction().setContextClassLoader(cl);
         }

         static void setContextClassLoader(Thread thread, ClassLoader cl)
         {
            getTCLAction().setContextClassLoader(thread, cl);
         }
      }

      TCLAction NON_PRIVILEGED = new TCLAction()
      {
         public ClassLoader getContextClassLoader()
         {
            return Thread.currentThread().getContextClassLoader();
         }

         public ClassLoader getContextClassLoader(Thread thread)
         {
            return thread.getContextClassLoader();
         }

         public void setContextClassLoader(ClassLoader cl)
         {
            Thread.currentThread().setContextClassLoader(cl);
         }

         public void setContextClassLoader(Thread thread, ClassLoader cl)
         {
            thread.setContextClassLoader(cl);
         }
      };

      TCLAction PRIVILEGED = new TCLAction()
      {
         private final PrivilegedAction getTCLPrivilegedAction = new PrivilegedAction()
         {
            public Object run()
            {
               return Thread.currentThread().getContextClassLoader();
            }
         };

         public ClassLoader getContextClassLoader()
         {
            return (ClassLoader) AccessController.doPrivileged(getTCLPrivilegedAction);
         }

         public ClassLoader getContextClassLoader(final Thread thread)
         {
            return (ClassLoader) AccessController.doPrivileged(new PrivilegedAction()
            {
               public Object run()
               {
                  return thread.getContextClassLoader();
               }
            });
         }

         public void setContextClassLoader(final ClassLoader cl)
         {
            AccessController.doPrivileged(new PrivilegedAction()
            {
               public Object run()
               {
                  Thread.currentThread().setContextClassLoader(cl);
                  return null;
               }
            });
         }

         public void setContextClassLoader(final Thread thread, final ClassLoader cl)
         {
            AccessController.doPrivileged(new PrivilegedAction()
            {
               public Object run()
               {
                  thread.setContextClassLoader(cl);
                  return null;
               }
            });
         }
      };

      ClassLoader getContextClassLoader();

      ClassLoader getContextClassLoader(Thread thread);

      void setContextClassLoader(ClassLoader cl);

      void setContextClassLoader(Thread thread, ClassLoader cl);
   }

   /**
    * Perform the MBeanServer.invoke op in a PrivilegedExceptionAction if
    * running with a security manager.
    */
   class MBeanServerAction implements PrivilegedExceptionAction
   {
      private ObjectName target;
      String method;
      Object[] args;
      String[] sig;

      MBeanServerAction()
      {
      }

      MBeanServerAction(ObjectName target, String method, Object[] args, String[] sig)
      {
         this.target = target;
         this.method = method;
         this.args = args;
         this.sig = sig;
      }

      public Object run() throws Exception
      {
         Object rtnValue = support.getServer().invoke(target, method, args, sig);
         return rtnValue;
      }

      Object invoke(ObjectName target, String method, Object[] args, String[] sig)
         throws Exception
      {
         SecurityManager sm = System.getSecurityManager();
         Object rtnValue = null;
         if (sm == null)
         {
            // Direct invocation on MBeanServer
            rtnValue = support.getServer().invoke(target, method, args, sig);
         }
         else
         {
            try
            {
               // Encapsulate the invocation in a PrivilegedExceptionAction
               MBeanServerAction action = new MBeanServerAction(target, method, args, sig);
               rtnValue = AccessController.doPrivileged(action);
            }
            catch (PrivilegedActionException e)
            {
               Exception ex = e.getException();
               throw ex;
            }
         }
         return rtnValue;
      }
   }
}
