/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.exoplatform.services.ldap.impl;

import org.exoplatform.commons.utils.PrivilegedSystemHelper;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.ldap.CreateObjectCommand;
import org.exoplatform.services.ldap.DeleteObjectCommand;
import org.exoplatform.services.ldap.LDAPService;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

/**
 * Created by The eXo Platform SAS .
 * Author : James Chamberlain james@echamberlains.com
 * Date: 11/2/2005
 */
public class LDAPServiceImpl implements LDAPService
{

   private static final Log LOG = ExoLogger.getLogger("exo.core.component.ldap.LDAPServiceImpl");

   private Map<String, String> env = new HashMap<String, String>();

   private int serverType = DEFAULT_SERVER;

   /**
    * @param params See {@link InitParams}
    */
   public LDAPServiceImpl(InitParams params)
   {
      LDAPConnectionConfig config = (LDAPConnectionConfig)params.getObjectParam("ldap.config").getObject();

      String url = config.getProviderURL();
      serverType = toServerType(config.getServerName());

      boolean ssl = url.toLowerCase().startsWith("ldaps");
      if (ssl)
      {
         // Enable the connection pool with the ssl protocol
         PrivilegedSystemHelper.setProperty("com.sun.jndi.ldap.connect.pool.protocol", "ssl");
      }
      env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
      env.put(Context.SECURITY_AUTHENTICATION, config.getAuthenticationType());
      env.put(Context.SECURITY_PRINCIPAL, config.getRootDN());
      env.put(Context.SECURITY_CREDENTIALS, config.getPassword());

      if (config.getTimeout() > 0)
      {
         PrivilegedSystemHelper.setProperty("com.sun.jndi.ldap.connect.pool.timeout",
            Integer.toString(config.getTimeout()));
      }

      if (config.getMinConnection() > 0)
      {
         PrivilegedSystemHelper.setProperty("com.sun.jndi.ldap.connect.pool.initsize",
            Integer.toString(config.getMinConnection()));
         PrivilegedSystemHelper.setProperty("com.sun.jndi.ldap.connect.pool.prefsize",
            Integer.toString(config.getMinConnection()));
      }

      if (config.getMaxConnection() > 0)
      {
         PrivilegedSystemHelper.setProperty("com.sun.jndi.ldap.connect.pool.maxsize",
            Integer.toString(config.getMaxConnection()));
      }

      env.put("com.sun.jndi.ldap.connect.pool", "true");
      env.put("java.naming.ldap.version", config.getVerion());
      env.put("java.naming.ldap.attributes.binary", "tokenGroups");
      env.put(Context.REFERRAL, config.getReferralMode());

      Pattern pattern = Pattern.compile("\\p{Space}*,\\p{Space}*", Pattern.CASE_INSENSITIVE);
      Matcher matcher = pattern.matcher(url);
      if (ssl)
         url = matcher.replaceAll("/ ldaps://");
      else
         url = matcher.replaceAll("/ ldap://");
      url += "/"; //NOSONAR
      env.put(Context.PROVIDER_URL, url);

      if (serverType == ACTIVE_DIRECTORY_SERVER && ssl)
         env.put(Context.SECURITY_PROTOCOL, "ssl");
   }

   /**
    * {@inheritDoc}
    */
   public LdapContext getLdapContext() throws NamingException
   {
      // This method can be used for getting context from thread-local variables,
      // etc. instead create new instance of LdapContext. Currently just create
      // new one (use from pool if 'com.sun.jndi.ldap.connect.pool' is 'true').
      // Override this method if need other behavior.
      return getLdapContext(true);
   }

   /**
    * {@inheritDoc}
    */
   public LdapContext getLdapContext(boolean renew) throws NamingException
   {
      // Force create new context.  
      return new InitialLdapContext(new Hashtable<String, String>(env), null);
   }

   /**
    * {@inheritDoc}
    */
   public void release(LdapContext ctx)
   {
      // Just close since we are not pooling anything by self.
      // Override this method if need other behavior.
      closeContext(ctx);
   }

   /**
    * {@inheritDoc}
    */
   public InitialContext getInitialContext() throws NamingException
   {
      Hashtable<String, String> props = new Hashtable<String, String>(env);
      props.put(Context.OBJECT_FACTORIES, "com.sun.jndi.ldap.obj.LdapGroupFactory");
      props.put(Context.STATE_FACTORIES, "com.sun.jndi.ldap.obj.LdapGroupFactory");
      return new InitialLdapContext(props, null);
   }

   /**
    * {@inheritDoc}
    */
   public boolean authenticate(String userDN, String password) throws NamingException
   {
      Hashtable<String, String> props = new Hashtable<String, String>(env);
      props.put(Context.SECURITY_AUTHENTICATION, "simple");
      props.put(Context.SECURITY_PRINCIPAL, userDN);
      props.put(Context.SECURITY_CREDENTIALS, password);
      props.put("com.sun.jndi.ldap.connect.pool", "false");

      InitialContext ctx = null;
      try
      {
         ctx = new InitialLdapContext(props, null);

         // anonymous user could be bind to AD but aren't able to pick up information
         return (ctx.lookup(userDN) != null);
      }
      catch (NamingException e)
      {
         LOG.debug("Error during initialization LDAP Context", e);
         return false;
      }
      finally
      {
         closeContext(ctx);
      }
   }

   /**
    * {@inheritDoc}
    */
   public int getServerType()
   {
      return serverType;
   }

   /**
    * Delete objects from context.
    * 
    * @param plugin see {@link DeleteObjectCommand} {@link ComponentPlugin}
    * @throws NamingException if {@link NamingException} occurs
    */
   public void addDeleteObject(ComponentPlugin plugin) throws NamingException
   {
   }

   /**
    * Create objects in context.
    * 
    * @param plugin see {@link CreateObjectCommand} {@link ComponentPlugin}
    * @throws NamingException if {@link NamingException} occurs
    */
   public void addCreateObject(ComponentPlugin plugin) throws NamingException
   {
      if (plugin instanceof CreateObjectCommand)
      {
         CreateObjectCommand command = (CreateObjectCommand)plugin;
         Map<String, Attributes> objectsToCreate = command.getObjectsToCreate();
         if (objectsToCreate == null || objectsToCreate.size() == 0)
            return;
         LdapContext ctx = getLdapContext();
         for (Map.Entry<String, Attributes> e : objectsToCreate.entrySet())
         {
            String name = e.getKey();
            Attributes attrs = e.getValue();
            try
            {
               try
               {
                  ctx.createSubcontext(name, attrs).close();
               }
               catch (CommunicationException e1)
               {
                  // release the previous context
                  release(ctx);
                  // create new LDAP context
                  ctx = getLdapContext(true);
                  // try repeat operation where communication error occurs
                  ctx.createSubcontext(name, attrs).close();
               }
               catch (ServiceUnavailableException e2)
               {
                  // release the previous context
                  release(ctx);
                  // do the same as for CommunicationException
                  ctx = getLdapContext(true);
                  //
                  ctx.createSubcontext(name, attrs).close();
               }
            }
            catch (Exception e3)
            {
               // Catch all exceptions here.
               // just inform about exception if it is not connection problem.
               LOG.error("Create object (" + name + ") failed. ", e3);
            }
         }
         release(ctx);
      }
   }

   private int toServerType(String name)
   {
      name = name.trim();
      if (name == null || name.length() < 1)
         return DEFAULT_SERVER;
      if (name.equalsIgnoreCase("ACTIVE.DIRECTORY"))
         return ACTIVE_DIRECTORY_SERVER;
      return DEFAULT_SERVER;
   }

   /**
    * Closes LDAP context and shows warning if exception occurred. 
    * 
    * @param ctx
    *          LDAP context
    */
   private void closeContext(Context ctx)
   {
      try
      {
         if (ctx != null)
         {
            ctx.close();
         }
      }
      catch (NamingException e)
      {
         LOG.warn("Exception occurred when tried to close context", e);
      }
   }

}
