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

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.ldap.Control;
import javax.naming.spi.ObjectFactory;

import org.jboss.system.ServiceMBeanSupport;
import org.jboss.util.Classes;

/**
 * A MBean that binds an arbitrary InitialContext into the JBoss default
 * InitialContext as a Reference. If RemoteAccess is enabled, the reference
 * is a Serializable object that is capable of creating the InitialContext
 * remotely. If RemoteAccess if false, the reference is to a nonserializable object
 * that can only be used from within this VM.
 *
 * @jmx:mbean extends="org.jboss.system.ServiceMBean"
 * 
 * @see org.jboss.naming.NonSerializableFactory
 * 
 * @version <tt>$Revision: 81030 $</tt>
 * @author  Scott.Stark@jboss.org
 * @author  <a href="mailto:jason@planet57.com">Jason Dillon</a>
 */
public class ExternalContext
   extends ServiceMBeanSupport
   implements ExternalContextMBean
{
   private boolean remoteAccess;
   private SerializableInitialContext contextInfo = new SerializableInitialContext();

   /**
    * No-args constructor for JMX.
    */
   public ExternalContext()
   {
      super();
   }

   public ExternalContext(String jndiName, String contextPropsURL)
      throws IOException, NamingException
   {   
      setJndiName(jndiName);
      setPropertiesURL(contextPropsURL);
   }

   /**
    * Set the jndi name under which the external context is bound.
    *
    * @jmx:managed-attribute
    */
   public String getJndiName()
   {
      return contextInfo.getJndiName();
   }

   /**
    * Set the jndi name under which the external context is bound.
    *
    * @jmx:managed-attribute
    */
   public void setJndiName(String jndiName) throws NamingException
   {
      contextInfo.setJndiName(jndiName);
      if( super.getState() == STARTED )
      {
         unbind(jndiName);
         try
         {
            rebind();
         }
         catch(Exception e)
         {
            NamingException ne = new NamingException("Failed to update jndiName");
            ne.setRootCause(e);
            throw ne;
         }
      }
   }

   /**
    * @jmx:managed-attribute
    */
   public boolean getRemoteAccess()
   {
      return remoteAccess;
   }
   
   /**
    * @jmx:managed-attribute
    */
   public void setRemoteAccess(final boolean remoteAccess)
   {
      this.remoteAccess = remoteAccess;
   }
   
   /**
    * @jmx:managed-attribute
    */
   public boolean getCacheContext()
   {
      return contextInfo.getCacheContext();
   }
   
   /**
    * @jmx:managed-attribute
    */
   public void setCacheContext(boolean cacheContext)
   {
      contextInfo.setCacheContext(cacheContext);
   }

   /**
    * Get the class name of the InitialContext implementation to
    * use. Should be one of:
    * <ul>
    *   <li>javax.naming.InitialContext
    *   <li>javax.naming.directory.InitialDirContext
    *   <li>javax.naming.ldap.InitialLdapContext
    * </ul>
    *
    * @jmx:managed-attribute
    * 
    * @return the classname of the InitialContext to use
    */
   public String getInitialContext()
   {
      return contextInfo.getInitialContext();
   }

   /**
    * Set the class name of the InitialContext implementation to
    * use. Should be one of:
    * <ul>
    *   <li>javax.naming.InitialContext
    *   <li>javax.naming.directory.InitialDirContext
    *   <li>javax.naming.ldap.InitialLdapContext
    * </ul>
    *
    * @jmx:managed-attribute
    *
    * @param contextClass, the classname of the InitialContext to use
    */
   public void setInitialContext(String className) throws ClassNotFoundException
   {
      contextInfo.loadClass(className);
   }

   /**
    * Set the InitialContex class environment properties from the given URL.
    *
    * @jmx:managed-attribute
    */
   public void setPropertiesURL(String contextPropsURL) throws IOException
   {
      contextInfo.loadProperties(contextPropsURL);
   }

   /**
    * Set the InitialContex class environment properties.
    *
    * @jmx:managed-attribute
    */
   public void setProperties(final Properties props) throws IOException
   {
      contextInfo.setProperties(props);
   }

   /**
    * Get the InitialContex class environment properties.
    *
    * @jmx:managed-attribute
    */
   public Properties getProperties() throws IOException
   {
      return contextInfo.getProperties();
   }
   
   /**
    * Start the service by binding the external context into the
    * JBoss InitialContext.
    */
   protected void startService() throws Exception
   {
      rebind();
   }

   /**
    * Stop the service by unbinding the external context into the
    * JBoss InitialContext.
    */
   protected void stopService() throws Exception
   {
      if( contextInfo.getCacheContext() )
         unbind(contextInfo.getJndiName());
   }

   private static Context createContext(Context rootContext, Name name) throws NamingException
   {
      Context subctx = rootContext;
      for(int n = 0; n < name.size(); n ++)
      {
         String atom = name.get(n);
         try
         {
            Object obj = subctx.lookup(atom);
            subctx = (Context) obj;
         }
         catch(NamingException e)
         {
            // No binding exists, create a subcontext
            subctx = subctx.createSubcontext(atom);
         }
      }

      return subctx;
   }

   private void rebind() throws Exception
   {
      Context ctx = contextInfo.newContext();
      Context rootCtx = (Context) new InitialContext();

      log.debug("ctx="+ctx+", env="+ctx.getEnvironment());
      
      // Get the parent context into which we are to bind
      String jndiName = contextInfo.getJndiName();
      Name fullName = rootCtx.getNameParser("").parse(jndiName);

      log.debug("fullName="+fullName);
      
      Name parentName = fullName;
      if( fullName.size() > 1 )
         parentName = fullName.getPrefix(fullName.size()-1);
      else
         parentName = new CompositeName();
      
      log.debug("parentName="+parentName);
      
      Context parentCtx = createContext(rootCtx, parentName);
      
      log.debug("parentCtx="+parentCtx);
      
      Name atomName = fullName.getSuffix(fullName.size()-1);
      String atom = atomName.get(0);
      boolean cacheContext = contextInfo.getCacheContext();
      
      if( remoteAccess == true )
      {
         // Bind contextInfo as a Referenceable
         parentCtx.rebind(atom, contextInfo);

         // Cache the context using NonSerializableFactory to avoid creating
         // more than one context for in VM lookups
         if( cacheContext == true )
         {
            // If cacheContext is true we need to wrap the Context in a
            // proxy that allows the user to issue close on the lookup
            // Context without closing the inmemory Context.
            ctx = CachedContext.createProxyContext(ctx);
            NonSerializableFactory.rebind(jndiName, ctx);
         }
      }
      else if( cacheContext == true )
      {
         // Bind a reference to the extern context using
         // NonSerializableFactory as the ObjectFactory. The Context must
         // be wrapped in a proxy that allows the user to issue close on the
         // lookup Context without closing the inmemory Context.

         Context proxyCtx = CachedContext.createProxyContext(ctx);
         NonSerializableFactory.rebind(rootCtx, jndiName, proxyCtx);
      }
      else
      {
         // Bind the contextInfo so that each lookup results in the creation
         // of a new Context object. The returned Context must be closed
         // by the user to prevent resource leaks.

         parentCtx.rebind(atom, contextInfo);
      }
   }

   private void unbind(String jndiName)
   {
      try
      {
         Context rootCtx = new InitialContext();
         Context ctx = (Context) rootCtx.lookup(jndiName);
         if( ctx != null )
            ctx.close();
         rootCtx.unbind(jndiName);
         NonSerializableFactory.unbind(jndiName);
      }
      catch(NamingException e)
      {
         log.error("unbind failed", e);
      }
   }

   /**
    * The external InitialContext information class. It acts as the
    * RefAddr and ObjectFactory for the external IntialContext and can
    * be marshalled to a remote client.
    */
   public static class SerializableInitialContext
      extends RefAddr
      implements Referenceable, Serializable, ObjectFactory
   {
      private static final long serialVersionUID = -6512260531255770463L;
      private String jndiName;
      private Class contextClass = javax.naming.InitialContext.class;
      private Properties contextProps;
      private boolean cacheContext = true;
      private transient Context initialContext;

      public SerializableInitialContext()
      {
         this("SerializableInitialContext");
      }
      
      public SerializableInitialContext(String addrType)
      {
         super(addrType);
      }

      public String getJndiName()
      {
         return jndiName;
      }
      
      public void setJndiName(final String jndiName)
      {
         this.jndiName = jndiName;
      }
      
      public boolean getCacheContext()
      {
         return cacheContext;
      }
      
      public void setCacheContext(final boolean cacheContext)
      {
         this.cacheContext = cacheContext;
      }
      
      public String getInitialContext()
      {
         return contextClass.getName();
      }
      
      public void loadClass(String className) throws ClassNotFoundException
      {
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
         contextClass = loader.loadClass(className);
      }

      public void setProperties(final Properties props)
      {
         contextProps = props;
      }

      public Properties getProperties()
      {
         return contextProps;
      }
      
      public void loadProperties(String contextPropsURL) throws IOException
      {
         InputStream is = null;
         contextProps = new Properties();

         // See if this is a URL we can load
         try
         {
            URL url = new URL(contextPropsURL);
            is = url.openStream();
            contextProps.load(is);
            return;
         }
         catch (IOException e)
         {   // Failed, try to locate a classpath resource below
            is = null;
         }

         is = Thread.currentThread().getContextClassLoader().getResourceAsStream(contextPropsURL);
         if( is == null )
         {
            throw new IOException("Failed to locate context props as URL or resource:"+contextPropsURL);
         }
         contextProps.load(is);
      }

      Context newContext() throws Exception
      {
         // First check the NonSerializableFactory cache
         initialContext = (Context) NonSerializableFactory.lookup(jndiName);
         // Create the context from the contextClass and contextProps
         if( initialContext == null )
            initialContext = newContext(contextClass, contextProps);
         return initialContext;
      }

      static Context newContext(Class contextClass, Properties contextProps)
         throws Exception
      {
         Context ctx = null;
         try
         {
            ctx = newDefaultContext(contextClass, contextProps);
         }
         catch(NoSuchMethodException e)
         {
            ctx = newLdapContext(contextClass, contextProps);
         }
         return ctx;
      }
      
      private static Context newDefaultContext(Class contextClass, Properties contextProps)
         throws Exception
      {
         Context ctx = null;
         Class[] types = {Hashtable.class};
         Constructor ctor = contextClass.getConstructor(types);
         Object[] args = {contextProps};
         ctx = (Context) ctor.newInstance(args);
         return ctx;
      }
      
      private static Context newLdapContext(Class contextClass, Properties contextProps)
         throws Exception
      {
         Context ctx = null;
         Class[] types = {Hashtable.class, Control[].class};
         Constructor ctor = contextClass.getConstructor(types);
         Object[] args = {contextProps, null};
         ctx = (Context) ctor.newInstance(args);
         return ctx;
      }
        
      public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment)
         throws Exception
      {
         Reference ref = (Reference) obj;
         SerializableInitialContext sic = (SerializableInitialContext) ref.get(0);
         return sic.newContext();
      }
        
      public Reference getReference() throws NamingException
      {
         Reference ref = new Reference(Context.class.getName(), this, this.getClass().getName(), null);
         return ref;
      }

      public Object getContent()
      {
         return null;
      }
   }

   /**
    * A proxy implementation of Context that simply intercepts the
    * close() method and ignores it since the underlying Context
    * object is being maintained in memory.
    */
   static class CachedContext implements InvocationHandler
   {
      Context externalCtx;
      
      CachedContext(Context externalCtx)
      {
         this.externalCtx = externalCtx;
      }

      static Context createProxyContext(Context ctx)
      {
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
         Class[] interfaces = Classes.getAllUniqueInterfaces(ctx.getClass());
         InvocationHandler handler = new CachedContext(ctx);
         Context proxyCtx = (Context) Proxy.newProxyInstance(loader, interfaces, handler);
         return proxyCtx;
      }

      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
      {
         Object value = null;
         if( method.getName().equals("close") )
         {
            // We just ignore the close method 
         }
         else
         {
            try
            {
               value = method.invoke(externalCtx, args);
            }
            catch(InvocationTargetException e)
            {
               throw e.getTargetException();
            }
         }
         return value;
      }
   }
}
