/*
 * 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.remoting;

import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;
import org.jboss.remoting.invocation.InternalInvocation;
import org.jboss.remoting.invocation.OnewayInvocation;
import org.jboss.remoting.loading.ClassBytes;
import org.jboss.remoting.loading.ClassRequestedMethodInvocationResult;
import org.jboss.remoting.loading.ClassRequiredMethodInvocation;
import org.jboss.remoting.loading.ClassUtil;
import org.jboss.remoting.ident.Identity;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.management.MBeanServer;

/**
 * ServerInvoker is the server-side part of a remote Invoker.  The ServerInvoker implementation is
 * responsible for calling transport, depending on how the protocol receives the incoming data.
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
 * @version $Revision: 1.12.2.8 $
 */
public abstract class ServerInvoker extends AbstractInvoker
{
   // since this class outputs a high volume of logging, only turn it on by setting system property
   private static final boolean logExtra = Boolean.getBoolean("jboss.remoting.debug");

   /**
    * Indicated the max number of threads used within oneway thread pool.
    */
   //TODO: figure out reasonable max -TME
   public static final int MAX_NUM_ONEWAY_THREADS = 500;

   protected Map handlers = new HashMap();
   protected Map callbackHandlers = new HashMap();
   private Map clientCallbackListener = new HashMap();
   private boolean started = false;
   private static PooledExecutor onewayExecutor;
   private static int onewayThreadCounter = 0;
   private MBeanServer server;

   // flags for if if should allow dynamic classloading
   /**
    * Client able to load class from server which is needed because the server returned
    * class from an invocation request that is not within client's classloader.
    */
   private boolean allowClientClassloading = false;
   /**
    * Server able to load class from client whihc is needed because the client passed
    * class in its invocation request that is not within server's classloader.
    */
   private boolean allowServerClassloading = false;


   public ServerInvoker(InvokerLocator locator)
   {
      super(locator);

   }

   protected MBeanServer getServer ()
   {
       return server;
   }

   private synchronized static Executor getOnewayExecutor()
   {
      if (onewayExecutor == null)
      {
         onewayExecutor = new PooledExecutor(MAX_NUM_ONEWAY_THREADS);
         onewayExecutor.setKeepAliveTime(3000);
         onewayExecutor.waitWhenBlocked();
         onewayExecutor.setThreadFactory(new ThreadFactory()
         {
            public Thread newThread(Runnable runnable)
            {
               return new Thread(runnable, "Remoting server oneway " + onewayThreadCounter++);
            }
         });
      }
      return onewayExecutor;
   }

   /**
    * return true if a server invocation handler has been registered for this subsystem
    *
    * @param subsystem
    * @return
    */
   public synchronized boolean hasInvocationHandler(String subsystem)
   {
      return handlers.containsKey(subsystem);
   }

   /**
    * return array of keys for each subsystem this invoker can handle
    *
    * @return
    */
   public synchronized String[] getSupportedSubsystems()
   {
      String subsystems [] = new String[handlers.size()];
      return (String[]) handlers.keySet().toArray(subsystems);
   }

   /**
    * return an array of the server invocation handlers
    *
    * @return
    */
   public synchronized ServerInvocationHandler[] getInvocationHandlers()
   {
      ServerInvocationHandler ih [] = new ServerInvocationHandler[handlers.size()];
      return (ServerInvocationHandler[]) handlers.values().toArray(ih);
   }

   /**
    * add a server invocation handler for a particular subsystem.  Typically, subsystems are defined
    * in org.jboss.remoting.Subsystem, however, this can be any string that the caller knows about.
    *
    * @param subsystem
    * @param handler
    */
   public synchronized void addInvocationHandler(String subsystem, ServerInvocationHandler handler)
   {
      handler.setInvoker(this);
      handlers.put(subsystem.toUpperCase(), handler);
   }

   /**
    * remove a subsystem invocation handler
    *
    * @param subsystem
    */
   public synchronized ServerInvocationHandler removeInvocationHandler(String subsystem)
   {
      return (ServerInvocationHandler) handlers.remove(subsystem.toUpperCase());
   }

   /**
    * get a ServerInvocationHandler for a given subsystem type
    *
    * @param subsystem
    * @return
    */
   public synchronized ServerInvocationHandler getInvocationHandler(String subsystem)
   {
      return (ServerInvocationHandler) handlers.get(subsystem.toUpperCase());
   }

   /**
    * called by the transport to handle unmarshalling, invocation, and re-marshalling the result back to the
    * client
    *
    * @param buf
    * @return
    * @throws IOException
    */
   public byte[] invoke(byte buf[])
         throws IOException
   {
      RemoteMethodInvocation invoke = null;
      String sessionId = null;
      ClassBytes arg = null;
      Object param = null;
      Map payload = null;
      InvokerLocator client = null;
      boolean preProcessed = false;

      try
      {
         // unmarshal the invocation buffer
         Object _invoke = ClassUtil.deserialize(buf, classbyteloader);

         if (logExtra && log.isDebugEnabled())
         {
            log.debug("server received invocation =>" + _invoke);
         }

         if (invoke == null && _invoke instanceof RemoteMethodInvocation)
         {
            // first invocation attempt
            invoke = (RemoteMethodInvocation) _invoke;
         }
         if (_invoke instanceof RemoteMethodInvocationResult)
         {
            RemoteMethodInvocationResult result = (RemoteMethodInvocationResult) _invoke;
            if (result.isException())
            {
               return result.serialize();
            }
            else if (result instanceof ClassRequestedMethodInvocationResult)
            {
               ClassRequestedMethodInvocationResult cr = (ClassRequestedMethodInvocationResult) result;
               if (logExtra && log.isDebugEnabled())
               {
                  log.debug("received requested client class: " + cr.getRequestedClass());
               }
               addClass(cr.getRequestedClass());
               // see this invocation back to the original
               _invoke = cr.getOriginalInvocation();
            }
         }
         if (_invoke instanceof RemoteMethodInvocation)
         {
            RemoteMethodInvocation mi = (RemoteMethodInvocation) _invoke;
            // see if there's a payload and process it first
            payload = mi.getPayload();
            sessionId = mi.getSessionId();
            arg = mi.getParameter();
            client = mi.getLocator();

            if (payload != null)
            {
               preProcessed = true;
               //???shouldn't arg be deserialized first?
               preProcess(sessionId, arg, payload, client);
            }

            if (_invoke instanceof ClassRequiredMethodInvocation)
            {
               // client tried to deserialize, and needs classes to continue
               ClassRequiredMethodInvocation cr = (ClassRequiredMethodInvocation) invoke;
               String cn = cr.getClassRequiredName();
               if (allowClientClassloading)
               {
                  byte classBytes[] = getClassBytes(cn);
                  if (logExtra && log.isDebugEnabled())
                  {
                     log.debug("client [" + client + "] needs class: " + cn + ", found?" + (classBytes != null));
                  }
                  if (classBytes == null)
                  {
                     RemoteMethodInvocationResult result = new RemoteMethodInvocationResult(sessionId, new ClassNotFoundException(cn), true, null);
                     return result.serialize();
                  }
                  ClassRequestedMethodInvocationResult crm = new ClassRequestedMethodInvocationResult(sessionId, new org.jboss.remoting.loading.ClassBytes(cn, classBytes), null);
                  return crm.serialize();
               }
               else
               {
                  throw new SecurityException("Server can not load and return class " + cn + " for client, per client request.  To allow this, " +
                        "please change security setting within configuration.");
               }
            }
            param = ClassUtil.deserialize(arg, classbyteloader);
            if (logExtra && log.isDebugEnabled()) log.debug("deserialized param: " + param);
            /*
              Object params[] = null;

              // attempt to deserialize parameters prior to invocation
              String sig[] = mi.getSignature ();
              if (sig != null)
              {
              for (int c = 0; c < sig.length; c++)
              {
              String cn = sig[c];
              loadClass (cn);
              }

              ClassBytes args[] = mi.getParameters ();
              if (args!=null)
              {
              params = new Object[sig.length];
              // we successfully loaded all the parameter classes, now deserialize parameter objects
              for (int c = 0; c < sig.length; c++)
              {
              if (args[c] != null)
              {
              // FIXME JGH NOTE: there is a problem here if the class doesn't
              // exist locally, need to fix
              params[c] = ClassUtil.deserialize (args[c], classbyteloader);
              }
              }
              }
              }
            */
            // OK, we made it pass deserialization
            Map returnPayload = new HashMap(1);
            try
            {
               if ("$PING$".equals(param))
               {
                  // if this is an invocation ping, just pong back
                  return new RemoteMethodInvocationResult(null, Identity.get(server), false, returnPayload).serialize();
               }


               InvocationRequest invocation =
                     new InvocationRequest(((RemoteMethodInvocation) _invoke).getSessionId(),
                           ((RemoteMethodInvocation) _invoke).getSubsystem(),
                           param,
                           payload,
                           returnPayload,
                           client);

               Object result = invoke(invocation);

               RemoteMethodInvocationResult value = new RemoteMethodInvocationResult(null, result, false, invocation.getReturnPayload());

               return value.serialize();
            }
            catch (Throwable throwable)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("invocation is returning an exception: " + throwable.getMessage() + " of type: " + throwable.getClass().getName());
               }
               RemoteMethodInvocationResult result = new RemoteMethodInvocationResult(null, throwable, true, returnPayload);
               return result.serialize();
            }
         }
      }
      catch (ClassNotFoundException cnf)
      {
         String className = cnf.getMessage();
         if (allowServerClassloading)
         {
            if (logExtra && log.isDebugEnabled())
            {
               log.debug("server needs class: " + className + ", asking client for it...");
            }
            ClassRequiredMethodInvocation result = new ClassRequiredMethodInvocation(null, className, locator);
            return result.serialize();
         }
         else
         {
            throw new SecurityException("Server can not ask client for class due to classloading failure since " + className +
                  " can not be found locally.  Change in security setting within configuration required.");
         }
      }
      catch (IOException io)
      {
         if (log.isDebugEnabled())
         {
            log.debug("exception during server invocation", io);
         }
         RemoteMethodInvocationResult result = new RemoteMethodInvocationResult(null, io, true, null);
         return result.serialize();
      }
      finally
      {
         if (preProcessed)
         {
            postProcess(sessionId, param, payload, client);
         }
      }
      throw new RuntimeException("ServerInvoker didn't properly find a result");
   }

   /**
    * Processes invocation request depending on the invocation type (internal, name based, oneway, etc).
    * Can be called on directly when client and server are local to one another (by-passing serialization)
    *
    * @param invocation
    * @return
    * @throws Throwable
    */
   public Object invoke(InvocationRequest invocation) throws Throwable
   {
      Object param = invocation.getParameter();
      Object result = null;

      if (param instanceof OnewayInvocation)
      {
         handleOnewayInvocation((OnewayInvocation) param, invocation);
      }
      else // no point in delaying return to client if oneway
      {
         String subsystem = invocation.getSubsystem();
         InvokerLocator client = invocation.getLocator();

         // too bad we can't optimize this a little better, since we take a lookup hit for
         // each invocation -JGH
         ServerInvocationHandler handler = (ServerInvocationHandler) handlers.get(subsystem.toUpperCase());
         if (handler == null)
         {
            throw new SubsystemNotSupported(subsystem, locator);
         }
         if (param instanceof InternalInvocation)
         {
            result = handleInternalInvocation((InternalInvocation) param, invocation, handler);

         }
         else
         {
            if (logExtra && log.isDebugEnabled())
            {
               log.debug("dispatching invocation: " + invocation + " to subsystem: " + subsystem + " from client: " + client);
            }

            result = handler.invoke(invocation);
         }
         if (logExtra && log.isDebugEnabled())
         {
            log.debug("dispatch invocation, returning back: " + result + " from subsystem: " + subsystem +
                  " to client: " + client);
         }
      }


      return result;
   }

   /**
    * Takes the real invocation from the client out of the OnewayInvocation and then executes the invoke()
    * with the real invocation on a seperate thread.
    *
    * @param onewayInvocation
    * @param invocation
    * @throws Throwable
    */
   private void handleOnewayInvocation(OnewayInvocation onewayInvocation, InvocationRequest invocation) throws Throwable
   {
      Object[] objs = onewayInvocation.getParameters();
      // The oneway invocation should contain the real param as it's only param in parameter array
      Object realParam = objs[0];
      invocation.setParameter(realParam);
      final InvocationRequest newInvocation = invocation;

      Executor executor = getOnewayExecutor();
      Runnable onewayRun = new Runnable()
      {
         public void run()
         {
            try
            {
               invoke(newInvocation);
            }
            catch (Throwable e)
            {
               // throw away exception since can't get it back to original caller
               log.error("Error executing server oneway invocation request: " + newInvocation, e);
            }
         }
      };
      executor.execute(onewayRun);
   }

   /**
    * Handles both internal and external invocations (internal meaning only
    * to be used within remoting and external for ones that go to handlers.
    *
    * @param param
    * @param invocation
    * @param handler
    * @return
    * @throws Throwable
    */
   private Object handleInternalInvocation(InternalInvocation param,
                                           InvocationRequest invocation,
                                           ServerInvocationHandler handler)
         throws Throwable
   {
      Object result = null;
      String methodName = param.getMethodName();
      if (log.isDebugEnabled())
      {
         log.debug("handling InternalInvocation where method name = " + methodName);
      }
      // check if the invocation is for callback handling
      if (InternalInvocation.ADDLISTENER.equals(methodName))
      {
         InvokerCallbackHandler callbackHandler = getCallbackHandler(invocation);
         handler.addListener(callbackHandler);
      }
      else if (InternalInvocation.REMOVELISTENER.equals(methodName))
      {
         ServerInvokerCallbackHandler callbackHandler = removeCallbackHandler(invocation);
         if (callbackHandler != null)
         {
            handler.removeListener(callbackHandler);
            callbackHandler.destroy();
         }
      }
      else if (InternalInvocation.GETCALLBACKS.equals(methodName))
      {
         ServerInvokerCallbackHandler callbackHandler = getCallbackHandler(invocation);
         result = callbackHandler.getCallbacks();
      }
      else if (InternalInvocation.ADDCLIENTLISTENER.equals(methodName))
      {
         String sessionId = invocation.getSessionId();
         Object[] params = param.getParameters();
         // the only element should be the callback handler
         if (params == null || params.length != 1)
         {
            log.error("Recieved addClientListener InternalInvocation, but getParameters() " +
                  "returned: " + params);
            throw new RuntimeException("InvokerCallbackHandler must be supplied as the only " +
                  "parameter object within the InternalInvocation when " +
                  "calling addClientListener.");
         }
         InvokerCallbackHandler callbackHandler = (InvokerCallbackHandler) params[0];
         clientCallbackListener.put(sessionId, callbackHandler);
      }
      else if (InternalInvocation.REMOVECLIENTLISTENER.equals(methodName))
      {
         String sessionId = invocation.getSessionId();
         clientCallbackListener.remove(sessionId);
      }
      else if (InternalInvocation.HANDLECALLBACK.equals(methodName))
      {
         String sessionId = invocation.getSessionId();
         InvokerCallbackHandler callbackHandler = (InvokerCallbackHandler) clientCallbackListener.get(sessionId);
         if (callbackHandler != null)
         {
            Object[] params = param.getParameters();
            InvocationRequest callbackRequest = (InvocationRequest) params[0];
            callbackHandler.handleCallback(callbackRequest);
         }
         else
         {
            log.error("Could not find callback handler to call upon for handleCallback " +
                  "where session id equals " + sessionId);
         }


      }
      else
      {
         log.error("Error processing InternalInvocation.  Unable to process method " +
               methodName + ".  Please make sure this should be an InternalInvocation.");
         throw new RuntimeException("Error processing InternalInvocation.  Unable to process method " +
               methodName);
      }
      return result;
   }

   //TODO: Do we really want to keep track of callback handlers here as well as
   // in the handler?  Seems like duplicate effort.  Might should just leave up
   // to handler to figure out duplicates and what to do with them? -TME
   private ServerInvokerCallbackHandler getCallbackHandler(InvocationRequest invocation) throws Exception
   {
      ServerInvokerCallbackHandler callbackHandler = null;
      String id = ServerInvokerCallbackHandler.getId(invocation);

      synchronized (callbackHandlers)
      {
         callbackHandler = (ServerInvokerCallbackHandler) callbackHandlers.get(id);
         // if does not exist, create it
         if (callbackHandler == null)
         {
            callbackHandler = new ServerInvokerCallbackHandler(invocation);
            callbackHandlers.put(id, callbackHandler);
         }
      }
      return callbackHandler;
   }

   private ServerInvokerCallbackHandler removeCallbackHandler(InvocationRequest invocation)
   {
      String id = ServerInvokerCallbackHandler.getId(invocation);
      ServerInvokerCallbackHandler callbackHandler = null;

      synchronized (callbackHandlers)
      {
         callbackHandler = (ServerInvokerCallbackHandler) callbackHandlers.remove(id);
      }
      return callbackHandler;
   }

   /**
    * returns true if the transport is bi-directional in nature, for example,
    * SOAP in unidirectional and SOCKETs are bi-directional (unless behind a firewall
    * for example).
    *
    * @return
    */
   public abstract boolean isTransportBiDirectional();


   /**
    * called prior to an invocation
    *
    * @param sessionId
    * @param payload
    * @param locator
    * @todo is sending in the arg appropriate???
    */
   protected void preProcess(String sessionId, ClassBytes arg, Map payload, InvokerLocator locator)
   {
   }

   /**
    * called after an invocation
    *
    * @param sessionId
    * @param payload
    * @param locator
    * @todo is sending in the arg appropriate???
    */
   protected void postProcess(String sessionId, Object param, Map payload, InvokerLocator locator)
   {
   }

   /**
    * subclasses should override to provide any specific start logic
    *
    * @throws IOException
    */
   public synchronized void start() throws IOException
   {
      started = true;
   }

   /**
    * return true if the server invoker is started, false if not
    *
    * @return
    */
   public synchronized boolean isStarted()
   {
      return started;
   }

   /**
    * subclasses should override to provide any specific stop logic
    */
   public synchronized void stop()
   {
      started = false;

      Iterator iter = handlers.values().iterator();
      while (iter.hasNext())
      {
         ServerInvocationHandler handler = (ServerInvocationHandler) iter.next();
         handler.destroy();
         iter.remove();
         handler = null;
      }
   }

   /**
    * Sets configuration for invoker.  Configuration might include information about dynamic classloading policy, etc.
    * <b>Example of mbean service xml entry:<p>
    * <mbean code="org.jboss.remoting.transport.Connector"
    * xmbean-dd="org/jboss/remoting/transport/Connector.xml"
    * name="jboss.remoting:service=Connector,transport=Socket"
    * display-name="Socket transport Connector">
    * <p/>
    * <attribute name="InvokerLocator">socket://localhost:8084</attribute>
    * <p/>
    * <attribute name="Configuration">
    * <config.
    * <classloading>
    * <server-side>true</server-side>
    * <client-side>true</client-side>
    * </classloading>
    * <handlers>
    * <handler subsystem="JMX">org.jboss.mx.remoting.JMXSubsystemInvocationHandler</handler>
    * </handlers>
    * </config>
    * </attribute>
    * <p/>
    * </mbean>
    * <p/>
    *
    * @param xml
    */
   public void setConfiguration(MBeanServer server, Element xml)
   {
      this.server = server;
      if (xml != null)
      {
         NodeList classloaderNode = xml.getElementsByTagName("classloading");
         if(classloaderNode != null && classloaderNode.getLength() > 0)
         {
            Element classloadingElm = (Element)classloaderNode.item(0);
            allowServerClassloading = getElementValue(classloadingElm, "server-side");
            allowClientClassloading = getElementValue(classloadingElm, "client-side");
         }
      }
   }

   private boolean getElementValue(Element xml, String tagName)
   {
      boolean tagValue = false;

      NodeList serverNode = xml.getElementsByTagName(tagName);
      if (serverNode != null && serverNode.getLength() > 0)
      {
         int len = serverNode.getLength();
         for (int x = 0; x < len; x++)
         {
            Node node = serverNode.item(x);
            String value = node.getFirstChild().getNodeValue();

            if (value != null && Boolean.valueOf(value).booleanValue())
            {
               tagValue = true;
            }
         }
      }
      return tagValue;
   }

   /**
    * destory the invoker permanently
    */
   //public void destroy () {}

}
