/* ================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and
 *     "Apache Jetspeed" must not be used to endorse or promote products
 *    derived from this software without prior written permission. For
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache" or
 *    "Apache Jetspeed", nor may "Apache" appear in their name, without
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.jetspeed.services.ldap;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NameNotFoundException;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.ReferralException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.servlet.ServletConfig;

// Turbine classes
import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineBaseService;
import org.apache.turbine.services.TurbineServices;
import org.apache.turbine.services.resources.ResourceService;

// Jetspeed classes
import org.apache.jetspeed.services.logging.JetspeedLogFactoryService;
import org.apache.jetspeed.services.logging.JetspeedLogger;

/**
 *
 * @author <a href="mailto:ender@kilicoglu.nom.tr">Ender KILICOGLU</a>
 * @author <a href="mailto:sami.leino@netorek.fi">Sami Leino</a>
 *
 * @version $Id: LDAPService.java,v 1.5 2003/07/23 19:50:21 morciuch Exp $ 
 * 
 */
public class LDAPService extends TurbineBaseService
{
    /**
     * Static initialization of the logger for this class
     */    
    private static final JetspeedLogger logger = JetspeedLogFactoryService.getLogger(LDAPService.class.getName());
    
    public static String SERVICE_NAME = "ldap";
    private static final String DEFAULT_ATTR[] = {
        "objectclass"
    };
    public static final int BASE = 0;
    public static final int ONE = 1;
    public static final int SUB = 2;
    public static final int DEFAULT_PORT = 389;
    public static final int DEFAULT_SSLPORT = 636;
    public static final int DEFAULT_LIMIT = 0;
    public static final int DEFAULT_TIMEOUT = 0;
    public static final int DEFAULT_VERSION = 3;
    private static String DEFAULT_CTX = "com.sun.jndi.ldap.LdapCtxFactory";

    private Hashtable connections;
    private Connector connector;
    private int limit;
    private int timeout;
    private int version;
    private String host;
    private int port;
    private int sslport;
    private String basedn;
    private String managerdn;
    private String password;
    private String managerlogin;
    private int batchsize;
    private String securityAuthentication;
    private String securityProtocol;
    private String socketFactory;
    private String saslclientpckgs;
    private String jndiprovider;
    private boolean anonymousBind;
    private String listFilter;
    private String attributesList[];
    private NameParser parser;
    private boolean showOpAttributes;
	private boolean useCachedDirContexts;
    private Properties env;

    /**
     * Main Connection Function
     *
     * Make first connection and store it in connections.
     *
     * @param url <code>LDAPURL</code> which locate server to connect.
     * @return boolean true if success else false.         
     */
    private boolean mainConnect(LDAPURL url)
    {
        setDefaultEnv();
        String base = url.getBase();
        env.put("java.naming.provider.url", base);
        try
        {
            DirContext ctx = new InitialDirContext(env);
            if (useCachedDirContexts)
            {
            	connections.put(basedn, ctx);
            }
            if(parser == null) parser = ctx.getNameParser("");
            return true;
        }
        catch(NamingException e)
        {
            logger.error ("LDAP Service: Failed to connect to " + url.getUrl(), e);
        }
        return false;
    }

    /**
     * Connection Function
     *
     * tries to connect given <code>LDAPURL</code>.
     *
     * @param url <code>LDAPURL</code> which locate server to connect.
     * @return DirContext connection context object.
     */
    public DirContext connect(LDAPURL url)
    {

        String base = url.getBase();
        DirContext ctx = (DirContext)connections.get(base);
        if(ctx != null)
        {
			// System.out.println("LDAPService: returning cached context.");
			// System.out.println("LDAPService: DN is " + url.getDN());
        	return ctx;
        }
		else
		{
			// System.out.println("LDAPService: creating new context for base " + base);
			// System.out.println("LDAPService: DN is " + url.getDN());
		}
		
        setDefaultEnv();
        env.put("java.naming.provider.url", base);
        do
        {
            try
            {
                ctx = new InitialDirContext(env);
                if (useCachedDirContexts) connections.put(base, ctx);
                return ctx;
            }
            catch(AuthenticationException e)
            {
                logger.error ("LDAP Service: Authentication error: " + base, e);
                if(connector == null)
                    return null;
                Properties pr = connector.referralConnection(env, url, anonymousBind);
                if(pr != null)
                {
                    env = pr;
                    continue;
                }
            }
            catch(CommunicationException e)
            {
                logger.error("LDAP Service: Communication error: " + base, e);
                if(connector == null)
                    return null;
                if(connector.connectionFailed(url))
                {
                    resetConnection(url);
                    continue;
                }
            }
            catch(NamingException e)
            {
                logger.error("LDAP Service:Failed to connect to " + base, e);
            }
            return ctx;
        } while(true);
    }

    /**
     * Reset Given Connection Function
     *
     * tries to connect given <code>LDAPURL</code>.
     *
     * @param url <code>LDAPURL</code> which locate server to connect.
     *
     */
    private void resetConnection(LDAPURL url)
    {
		// System.out.println("LDAPService: resetConnection() called.");
        connections.remove(url.getBase());
    }
     /**
     * Set Default Environment
     *
     * Fill properties necessary to connect.
     *
     */
    private void setDefaultEnv()
    {
        showOpAttributes = attributesList != null;
        env.put("java.naming.referral", "ignore");
        env.put("java.naming.batchsize", String.valueOf(batchsize));

        if(anonymousBind)
        {
            env.remove("java.naming.security.principal");
            env.remove("java.naming.security.credentials");
        }
        else
        {
            env.put("java.naming.security.principal", managerdn);
            env.put("java.naming.security.credentials", password);
        }

        env.put("java.naming.security.authentication", securityAuthentication);
        if(saslclientpckgs  != null)
		{
		    env.put("javax.security.sasl.client.pkgs", saslclientpckgs);
		}
		else
        {
            env.remove("javax.security.sasl.client.pkgs");
        }

        env.put("java.naming.ldap.derefAliases", "never");
        env.put("java.naming.ldap.deleteRDN", "true" );
        env.put("java.naming.ldap.version", String.valueOf(version));

        if( securityProtocol != null)
        {
            env.put("java.naming.security.protocol", securityProtocol);
            if(securityProtocol.equalsIgnoreCase("ssl"))
            {
                env.put("java.naming.ldap.factory.socket", socketFactory );
			}
        }
        else
        {
            env.remove("java.naming.security.protocol");
            env.remove("java.naming.ldap.factory.socket");
        }

		// env.put("com.sun.jndi.ldap.trace.ber", System.err);
        env.put("java.naming.factory.initial", (Object)(jndiprovider));
    }

    /**
     * Disconnection Function
     *
     * tries to disconnect all connection.
     *
     * @return boolean true if success else false.
     */

    public boolean disconnect()
    {
		// System.out.println("LDAPService: disconnect() called.");
        DirContext ctx = null;

        for(Enumeration enum = connections.elements(); enum.hasMoreElements();)
		{
		    try
            {
                ctx = (DirContext)enum.nextElement();
                ctx.close();
            }
            catch(NamingException e)
            {
                logger.error("LDAP Service: Disconnect failed", e);
            }
		}
		
        connections.clear();
        return true;
    }

    public boolean checkAndCloseContext(Context context)
    {
		try
        {
            if (!useCachedDirContexts)
            {
            	context.close();
            	// System.out.println("LDAPService: closeContext() called.");
            }
            else
            {
            	// System.out.println("LDAPService: context left in cache.");
            }
	        return true;
        }
        catch(NamingException e)
        {
            logger.error("LDAP Service: closeContext() failed", e);
	        return false;
        }
    }


    /**
     * Delete Atrribute Function
     *
     * Delete given attribute for given <code>LDAPURL</code>.
     *
     * @param url object affected.
     * @param at Atribute to delete
     * @return boolean true if success else false.
     */

    public boolean deleteAttribute(LDAPURL url, Attribute at)
    {
        try
        {
            ModificationItem mods[] = new ModificationItem[1];
            mods[0] = new ModificationItem(3, at);
            return modifyAttribute(url, mods);
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to delete '" + at.getID() + "' attribute for " + url.getUrl(), e);
        }
        return false;
    }

    /**
     * Add Attribute Function
     *
     * add given attribute to given <code>LDAPURL</code>.
     *
     * @param url object affected.
     * @param at Atribute to add
     * @return boolean true if success else false.
     */
    public boolean addAttribute(LDAPURL url, Attribute at)
    {
        try
        {
            ModificationItem mods[] = new ModificationItem[1];
            mods[0] = new ModificationItem(1, at);
            return modifyAttribute(url, mods);
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to add '" + at.getID() + "' attribute for " + url.getUrl(), e);
        }
        return false;
    }

    /**
     * Add entry Function
     *
     * tries to add object with given <code>LDAPURL</code> and
     * with given attributes.
     *
     * @param url object to create.
     * @param at Atributes to add
     * @return boolean true if success else false.
     */
    public boolean addEntry(LDAPURL url, Attributes at)
    {
        DirContext ctx = connect(url);

        if(ctx == null)
            return false;
        try
        {
            ctx.createSubcontext(url.getDN(), at);
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            return addEntry(myurl, at);
        }
        catch(NamingException e)
        {

e.printStackTrace();

            logger.error("LDAP Service: Failed to add new entry " + url.getDN(), e);
            return false;
        }
        return true;
    }

    /**
     * Query existense of an Object Function
     *
     * tries to locate given <code>LDAPURL</code>.
     *
     * @param url object affected.
     * @return boolean true if exist else false.
     */
    public boolean exists(LDAPURL url)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            NamingEnumeration results = search(ctx, url.getDN(), "(objectclass=*)", DEFAULT_ATTR, 0, false);
            checkAndCloseContext(ctx);
            return true;
        }
        catch(NameNotFoundException _ex)
        {
            return false;
        }
        catch(NamingException _ex)
        {
            return false;
        }
    }

    /**
     * Compare Function
     *
     * Compare given <code>LDAPURL</code>s.
     *
     * @param srcUrl object affected.
     * @param dstUrl object affected.
     * @return int 0 same host+DN, 1 same DN,2 child,3 no relation.
     */
    public int compare(LDAPURL srcUrl, LDAPURL dstUrl)
    {
        if(!srcUrl.sameHosts(dstUrl))
            return 0;
        Name src = parse(srcUrl.getDN());
        Name dst = parse(dstUrl.getDN());
        if(dst.compareTo(src) == 0)
            return 1;
        if(dst.startsWith(src))
            return 2;
        Name prefix = src.getPrefix(src.size() - 1);
        return dst.compareTo(prefix) != 0 ? 0 : 3;
    }

    /**
     * Import Function
     *
     * Import given <code>LDAPURL</code> to another dn.
     *
     * @param url object to import.
     * @param dn Dn of new object.
     * @param entry attributes.
     * @param type 0 addnew, 1 update, 2 sync.
     * @return int 1 success, 0 unknown type,-1 failure.
     */
    public int importEntry(LDAPURL url, String dn, Attributes entry, int type)
    {
        boolean rs = false;
        LDAPURL myurl = new LDAPURL(url.getHost(), url.getPort(), dn);
        if(type == 0)
            rs = addEntry(myurl, entry);
        else

        if(type == 1)
            rs = updateEntry(myurl, entry);
        else
        if(type == 2)
            rs = synchEntry(myurl, entry);
        else
            return 0;
        return !rs ? -1 : 1;
    }

    /**
     * Modify Function
     *
     * Modify given <code>LDAPURL</code> with fiven modification items.
     *
     * @param url object to modify.
     * @param mods Modification items.
     * @exception NamingException
     * @return boolean true if success else false.
     */
    private boolean modifyAttribute(LDAPURL url, ModificationItem mods[])
        throws NamingException
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            ctx.modifyAttributes(url.getDN(), mods);
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            return modifyAttribute(myurl, mods);
        }
        return true;
    }

    /**
     * Build LDAPURL Function
     *
     * Build <code>LDAPURL</code> with given DN.
     *
     * @param DN DN value for object.
     * @return LDAPURL build with given DN.
     */
    public LDAPURL buildURL(String DN)
    {
      return new LDAPURL(host,port,DN + "," + basedn);
    }

    /**
     * Read Attributes Function
     *
     * Return attributes for given <code>LDAPURL</code>.
     *
     * @param url object to read attributes.
     * @return Attributes attributes for given url.
     */
    public Attributes read(LDAPURL url)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return null;
        
        Attributes attrs = null;
        try
        {
            if(showOpAttributes)
            {
                attrs = ctx.getAttributes(url.getDN(), attributesList);
            }
            else
            {
                attrs = ctx.getAttributes(url.getDN());
            }
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            if(myurl.getDN().length() == 0)
            {
                myurl.setDN(url.getDN());
            }
            return read(myurl);
        }
        catch(CommunicationException e)
        {
            if(connector == null)
            {
                logger.debug("LDAP Service: Communication error : " + url.getBase(), e);
                return null;
            }
            if(connector.connectionFailed(url))
            {
                resetConnection(url);
			}
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to read entry " + url.getDN(), e);
            return null;
        }
        return attrs;
    }

    /**
     * Rename Entry Function
     *
     * Rename given <code>LDAPURL</code> with given DN.
     *
     * @param url object to modify.
     * @param newDN DN value for new object.
     * @return boolean true if success else false.
     */
    public boolean renameEntry(LDAPURL url, String newDN)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            ctx.rename(url.getDN(), newDN);
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            logger.debug("LDAP Service: Failed to rename entry. (not supported for referrals)", e);
            return false;
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to rename entry " + url.getDN(), e);
            return false;
        }
        return true;
    }

    /**
     * Sync Entry Function
     *
     * Sync given <code>LDAPURL</code> with given atrributes.
     *
     * @param url object to sync.
     * @param ats Modification items.
     * @return boolean true if success else false.
     */
    public boolean synchEntry(LDAPURL url, Attributes ats)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            ctx.modifyAttributes(url.getDN(), 2, ats);
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            return synchEntry(url, ats);
        }
        catch(NameNotFoundException _ex)
        {
            try
            {
                ctx.createSubcontext(url.getDN(), ats);
            }
            catch(NamingException _ex2)
            {
                return false;
            }
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to synchronize entries", e);
            return false;
        }
        return true;
    }

    /**
     * Delete Attributes Function
     *
     * Delete Attributes for given <code>LDAPURL</code>.
     *
     * @param url object to modify.
     * @param ats Attributes to delete.
     * @return boolean true if success else false.
     */
    public boolean deleteAttrs(LDAPURL url, Attributes ats)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            ctx.modifyAttributes(url.getDN(), DirContext.REMOVE_ATTRIBUTE, ats);
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            return synchEntry(url, ats);
        }
        catch(NameNotFoundException _ex)
        {
            try
            {
                ctx.createSubcontext(url.getDN(), ats);
	            checkAndCloseContext(ctx);
            }
            catch(NamingException _ex2)
            {
                return false;
            }
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to delete Attributes", e);
            return false;
        }
        return true;
    }

    /**
     * Delete Entry Function
     *
     * Delete given <code>LDAPURL</code>.
     *
     * @param url object to delete.
     * @return boolean true if success else false.
     */
    public boolean deleteEntry(LDAPURL url)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            ctx.destroySubcontext(url.getDN());
            checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            return deleteEntry(myurl);
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to delete entry " + url.getDN(), e);
            return false;
        }
        return true;
    }

    /**
     * Find Entry Name Function
     *
     * Return entry name for given <code>LDAPURL</code>.
     *
     * @param url object to modify.
     * @return LDAPURL real entry DN.
     */
    public LDAPURL findEntryName(LDAPURL url)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return null;
        
        Name name = parse(url.getDN());
        String base = name.getPrefix(name.size() - 1).toString();
        String dn = url.getDN();
        String rdn = name.get(name.size() - 1).toString();
        int i = 1;
        boolean foundName = true;

        while(foundName)
        {
            try
            {
                NamingEnumeration results = search(ctx, dn, "(objectclass=*)", DEFAULT_ATTR, 0, false);
                if(i == 1)
                    rdn = rdn + " copy";
                else
                if(i == 2)
                    rdn = rdn + " " + i;
                else
                if(i >= 3)
                    rdn = rdn.substring(0, rdn.length() - 1) + i;
                dn = rdn + ", " + base;
                i++;
            }
            catch(NameNotFoundException _ex)
            {
                foundName = false;
                return new LDAPURL(url.getHost(), url.getPort(), dn);
            }
            catch(NamingException _ex)
            {
                return null;
            }
        }
        
        checkAndCloseContext(ctx);

        return null;
    }
    
    /**
     * Delete Tree Function
     *
     * Delete record with all child node <code>LDAPURL</code>.
     *
     * @param url object to modify.
     * @return boolean true if success else false.
     */
    public boolean deleteTree(LDAPURL url)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        String entryDN = null;
        LDAPURL myurl = null;
        String baseDN = url.getDN();

        try
        {
            for(NamingEnumeration results = search(ctx, baseDN, "(objectclass=*)", DEFAULT_ATTR, 1, false); results.hasMore();)
            {
                SearchResult si = (SearchResult)results.next();
                entryDN = getFixedDN(si.getName(), baseDN);
                myurl = new LDAPURL(url.getHost(), url.getPort(), entryDN);
                if(!deleteTree(myurl))
                {
                    return false;
                }
            }

			checkAndCloseContext(ctx);
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Delete tree failed", e);
            return false;
        }
        return deleteEntry(url);
    }

    /**
     * Transfer Function
     *
     * Transfer given <code>LDAPURL</code> to other <code>LDAPURL</code>.
     *
     * @param fromUrl object to transfer.
     * @param toUrl target object.
     * @param delete delete after transfer.
     * @param replace replace if exist.
     * @param withChildren transfer with childs.
     * @return boolean true if success else false.
     */
    public boolean transfer(LDAPURL fromUrl, LDAPURL toUrl, boolean delete, boolean replace, boolean withChildren)
    {
        LDAPURL dstUrl = toUrl;
        int rc = compare(fromUrl, toUrl);
        if(rc == 1)
            dstUrl = findEntryName(dstUrl);
        if(withChildren)
            return transferTreeSub(fromUrl, dstUrl, delete, replace);
        else
            return transferEntry(fromUrl, dstUrl, delete, replace);

    }

    /**
     * Transfer with updates Function
     *
     * Transfer updated <code>LDAPURL</code> with given modification items
     * to other <code>LDAPURL</code>.
     *
     * @param fromUrl object to transfer.
     * @param toUrl target object.
     * @param delete delete after transfer.
     * @param replace replace if exist.
     * @param ats attributes to update.
     * @return boolean true if success else false.
     */
    public boolean transferEntry(LDAPURL fromUrl, Attributes ats, LDAPURL toUrl, boolean delete, boolean replace)
    {
        if(delete && !deleteEntry(fromUrl))
            return false;
        if(updateEntry(toUrl, ats, replace))
            return true;
        if(delete)
            addEntry(fromUrl, ats);
        return false;
    }

    /**
     * Transfer without updates Function
     *
     * Transfer <code>LDAPURL</code> to other <code>LDAPURL</code>.
     *
     * @param fromUrl object to transfer.
     * @param toUrl target object.
     * @param delete delete after transfer.
     * @param replace replace if exist.
     * @return boolean true if success else false.
     */

    public boolean transferEntry(LDAPURL fromUrl, LDAPURL toUrl, boolean delete, boolean replace)
    {
        Attributes ats = read(fromUrl);
        if(ats == null)
            return false;
        else
            return transferEntry(fromUrl, ats, toUrl, delete, replace);
    }

    /**
     * Transfer Tree Function
     *
     * Transfer <code>LDAPURL</code> with all child to other <code>LDAPURL</code>.
     *
     * @param fromUrl object to transfer.
     * @param toUrl target object.
     * @param delete delete after transfer.
     * @param replace replace if exist.
     * @return boolean true if success else false.
     */
    private boolean transferTreeSub(LDAPURL fromUrl, LDAPURL toUrl, boolean delete, boolean replace)
    {
        DirContext ctx = connect(fromUrl);
        if(ctx == null) return false;

        Attributes ats = read(fromUrl);
        if(ats == null) return false;
        
        String srcDN = fromUrl.getDN();
        String dstDN = toUrl.getDN();
        boolean createdBase = false;
        boolean rc = false;
        boolean moreReferrals = true;

        while(moreReferrals)
        {
            try
            {
                NamingEnumeration results = search(ctx, srcDN, "(objectclass=*)", DEFAULT_ATTR, 1, false);
                if(!results.hasMore())
                {
                    if(!transferEntry(fromUrl, ats, toUrl, delete, replace))
                        return false;
                } else
                {
                    String name = null;
                    if(!createdBase)
                    {
                        if(!updateEntry(toUrl, ats, replace))
                            return false;
                        createdBase = true;
                    }
                    LDAPURL srcUrl;
                    LDAPURL dstUrl;
                    for(; results.hasMore(); transferTreeSub(srcUrl, dstUrl, delete, replace))
                    {
                        SearchResult si = (SearchResult)results.next();
                        name = fixName(si.getName());
                        String tmpSrcDN = getDN(name, srcDN);
                        srcUrl = new LDAPURL(fromUrl.getHost(), fromUrl.getPort(), tmpSrcDN);
                        String tmpDstDN = getDN(name, dstDN);
                        dstUrl = new LDAPURL(toUrl.getHost(), toUrl.getPort(), tmpDstDN);
                    }

                    if(delete && !deleteEntry(fromUrl))
                        return false;
                }
                moreReferrals = false;
            }
            catch(ReferralException e)
            {
                if(delete)
                {
                    moreReferrals = false;
                }
               	else
                {
                    if(!createdBase)
                    {
                        if(!updateEntry(toUrl, ats, replace)) return false;
                        createdBase = true;
                    }

                    LDAPURL srcUrl = getReferralUrl(e);
                    String tmpDstDN = getName(srcUrl.getDN()) + ", " + dstDN;
                    LDAPURL dstUrl = new LDAPURL(toUrl.getHost(), toUrl.getPort(), tmpDstDN);
                    boolean rs = transferTreeSub(srcUrl, dstUrl, delete, replace);
                    if(!rs)return false;

                    moreReferrals = e.skipReferral();
                    try
                    {
                    	// Close old context
                    	checkAndCloseContext(ctx);
                        ctx = (DirContext)e.getReferralContext();
                    }
                    catch(NamingException _ex) { }
                }
            }
            catch(NamingException e)
            {
                logger.debug("LDAP Service: Transfer Tree failed", e);
                return false;
            }
        }

        checkAndCloseContext(ctx);
        return true;
    }

    /**
     * Update Atribute Function
     *
     * Update an attribute for given <code>LDAPURL</code>.
     *
     * @param url object to update.
     * @param at atrribute to update.
     * @return boolean true if success else false.
     */
    public boolean updateAttribute(LDAPURL url, Attribute at)
    {
        try
        {
            ModificationItem mods[] = new ModificationItem[1];
            mods[0] = new ModificationItem(2, at);
            return modifyAttribute(url, mods);
        }
        catch(NamingException e)
        {
            logger.debug("LDAP Service: Failed to update '" + at.getID() + "' attribute for " + url.getUrl(), e);
        }
        return false;
    }

    /**
     * Update Atributes Function
     *
     * Update attributes for given <code>LDAPURL</code>.
     *
     * @param url object to update.
     * @param at atrributes to update.
     * @return boolean true if success else false.
     */
    public boolean updateEntry(LDAPURL url, Attributes at)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;

        try
        {
            ctx.modifyAttributes(url.getDN(), 2, at);
			checkAndCloseContext(ctx);
        }
        catch(ReferralException e)
        {
            LDAPURL myurl = getReferralUrl(e);
            return updateEntry(myurl, at);
        }
        catch(NamingException e)
        {
            logger.error("LDAP Service: Failed to update entry " + url.getDN(), e);
            return false;
        }
        return true;
    }
 
    /**
     * Update Entry Function
     *
     * Update attributes for given <code>LDAPURL</code>.
     *
     * @param url object to update.
     * @param ats atrributes to update.
     * @param replace replace if exist.
     * @return boolean true if success else false.
     */
    public boolean updateEntry(LDAPURL url, Attributes ats, boolean replace)
    {
        return replace ? synchEntry(url, ats) : addEntry(url, ats);
    }

    /**
     * Search Function
     *
     * Search objects for given Base DN and filter.
     *
     * @param ctx directory context.
     * @param dn Base search DN.
     * @param filter Search filter.
     * @param attribs attributes to receive.
     * @param type search scope 1 Subscope, else 0.
     * @exception NamingException
     * @return NamingEnumeration Results.
     */
    public NamingEnumeration search(DirContext ctx, String dn, String filter, String attribs[], int type)
        throws NamingException
    {
		return search(ctx, dn, filter, attribs, type, true);
    }

    /**
     * Search Function
     *
     * Search objects for given Base DN and filter.
     *
     * @param ctx directory context.
     * @param dn Base search DN.
     * @param filter Search filter.
     * @param attribs attributes to receive.
     * @param type search scope 2 Subscope, else 1.
     * @param setLimits enable limits.
     * @exception NamingException
     * @return NamingEnumeration Results.
     */
    private NamingEnumeration search(DirContext ctx, String dn, String filter, String attribs[], int type, boolean setLimits)
        throws NamingException
    {
        SearchControls constraints = new SearchControls();
        constraints.setSearchScope(type);
        constraints.setReturningAttributes(attribs);
        if(setLimits)
        {
            constraints.setCountLimit(limit);
            constraints.setTimeLimit(timeout);
        }
        NamingEnumeration results = ctx.search(dn, filter, constraints);
        return results;
    }

    /**
     * Search Function
     *
     * Search objects for given BaseURL and filter.
     *
     * @param url Base URL .
     * @param filter Search filter.
     * @param attribs attributes to receive.
     * @param subTreeScope true subtree else false.
     * @return Vector Results.
     */
    public Vector search(LDAPURL url, String filter, String attribs[], boolean subTreeScope)
    {
    	/*
		System.out.println("===== LDAPService: search");
		System.out.println("===== LDAPService: " + url);
		System.out.println("===== LDAPService: " + filter);
		System.out.println("===== LDAPService: " + attribs);
		System.out.println("===== LDAPService: " + subTreeScope);
		*/
		
        Vector results = new Vector();
        String attrs[] = new String[attribs.length + 1];
        attrs[0] = "objectclass";
        System.arraycopy(attribs, 0, attrs, 1, attribs.length);
        int scope = subTreeScope ? 2 : 1;
        subSearch(url, filter, attrs, scope, results);

        return results;
    }

    /**
     * Search Function
     *
     * Search objects for given BaseURL and filter.
     *
     * @param url Base URL .
     * @param filter Search filter.
     * @param attribs attributes to receive.
     * @param scope true subtree else false.
     * @param rs Result
     * @return boolean true if success else false.
     */
    private boolean subSearch(LDAPURL url, String filter, String attribs[], int scope, Vector rs)
    {
        DirContext ctx = connect(url);
        if(ctx == null) return false;
        
        String entryDN = null;
        Attributes at = null;
        Attribute a = null;
        LDAPURL myurl = null;
        int subscope = 0;
        String baseDN = url.getDN();

        boolean moreReferrals = true;
        while(moreReferrals)
		{
		    try
            {
                Vector vl;
                for(NamingEnumeration results = search(ctx, baseDN, filter, attribs, scope); results.hasMore(); rs.addElement(vl))
                {
                    SearchResult si = (SearchResult)results.next();
                    vl = new Vector(attribs.length);
                    entryDN = getFixedDN(si.getName(), baseDN);
                    myurl = new LDAPURL(url.getHost(), url.getPort(), entryDN);
                    vl.addElement(myurl);
                    at = si.getAttributes();
                    for(int i = 1; i < attribs.length; i++)
                    {
                        a = at.get(attribs[i]);
                        if(a == null)
                        {
                            vl.addElement("N/A");
                        } else
                        {
                            Object v = a.get();
                            if(v instanceof byte[])
                                vl.addElement(v);
                            else
                                vl.addElement(a.get().toString());
                        }
                    }
                }
                moreReferrals = false;
            }

            catch(ReferralException e)
            {
                myurl = getReferralUrl(e);
                subscope = scope != 1 ? scope : 0;
                boolean error = subSearch(myurl, filter, attribs, subscope, rs);
                if(!error) return error;
                
                moreReferrals = e.skipReferral();
                try
                {
                   	// Close old context
                   	checkAndCloseContext(ctx);
                    ctx = (DirContext)e.getReferralContext();
                }
                catch(NamingException _ex) { }
            }
            catch(NamingException e)
            {
                logger.debug("LDAP Service: Search failed", e);
                return false;
            }
        }
        
       	checkAndCloseContext(ctx);
        return true;
    }

    /**
     * Get value Function
     *
     * Return value for attribute value pair.
     *
     * @param attrvalue input.
     * @return String Value.
     */
    public String removeAttrName(String attrvalue)
    {
        StringTokenizer token = new StringTokenizer(attrvalue,"=");
        if (token.countTokens()==2)
        {
        	token.nextToken();
        	return token.nextToken();
        }
        else
        {
            return attrvalue;
        }
    }
 
    /**
     * Return full DN Function
     *
     * Add Base DN to given DN.
     *
     * @param rdn full DN.
     * @param base Base DN.
     * @return String DN.
     */
    private String getFixedDN(String rdn, String base)
    {
        return getDN(fixName(rdn), base);
    }

    /**
     * Return Name Function
     *
     * Return name for given DN.
     *
     * @param dn DN.
     * @return String Name.
     */
    public String getName(String dn)
    {
        try
        {
            Name nm = parser.parse(dn);
            return nm.get(nm.size() - 1).toString();
        }
        catch(NamingException _ex)
        {
            return null;
        }
    }

    /**
     * Fix Name Function
     *
     * Fix chars .
     *
     * @param name Name to fix.
     * @return String Fixed name.
     */
    private String fixName(String name)
    {
        if(name.length() > 0 && name.charAt(0) == '"')
        {
            int size = name.length() - 1;
            StringBuffer buf = new StringBuffer();
            for(int i = 1; i < size; i++)
            {
                if(name.charAt(i) == '/')
                    buf.append("\\");
                buf.append(name.charAt(i));
            }

            return buf.toString();
        }
        else
        {
            return name;
        }
    }

    /**
     * Return full DN Function
     *
     * Add Base DN to given DN.
     *
     * @param rdn DN.
     * @param base Base DN.
     * @return String full DN.
     */
    private String getDN(String rdn, String base)
    {
        if(rdn.length() == 0)
            return base;
        if(base.length() == 0)
            return rdn;
        else
            return rdn + ", " + base;
    }

    /**
     * Return Name Function
     *
     * Add Base DN to given DN.
     *
     * @param dn full DN.
     * @return Name Name for given DN.
     */
    public Name parse(String dn)
    {
        try
        {
            return parser.parse(dn);
        }
        catch(NamingException _ex)
        {
            return null;
        }
    }

    /**
     * Get Referral URL Function
     *
     * Return <code>LDAPURL</code> extracted from exception.
     *
     * @param e Exception to extract.
     * @return LDAPURL referrral URL.
     */
    public LDAPURL getReferralUrl(ReferralException e)
    {
        String url = (String)e.getReferralInfo();
        try
        {
            return new LDAPURL(url);
        }
        catch(Exception ex)
        {
            logger.debug("Invalid url: " + ex.getMessage() + " " + url);
        }
        return null;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Service Init
    ///////////////////////////////////////////////////////////////////////////

    /**
     * This is the early initialization method called by the
     * Turbine <code>Service</code> framework
     * @param conf The <code>ServletConfig</code>
     * @exception InitializationException if the service fails to initialize
     */
    public void init( ServletConfig conf ) throws InitializationException
    {
        connections = new Hashtable();
        connector = null;
        parser = null;
        env = new Properties();
        ResourceService serviceConf = ((TurbineServices)TurbineServices.getInstance())
                                                     .getResources(SERVICE_NAME);
        this.host = serviceConf.getString("host");
        this.port = serviceConf.getInt("port",DEFAULT_PORT);
        this.sslport = serviceConf.getInt("sslport",DEFAULT_SSLPORT);
        this.limit = serviceConf.getInt("limit",DEFAULT_LIMIT);
        this.timeout = serviceConf.getInt("timeout",DEFAULT_TIMEOUT);
        this.version = serviceConf.getInt("version",DEFAULT_VERSION);
        this.listFilter = repair(serviceConf.getString("listfilter","(objectclass=*)"));
        this.basedn = repair(serviceConf.getString("basedn"));
        this.managerdn = repair(serviceConf.getString("managerdn"));
        this.password = serviceConf.getString("password");
        this.attributesList = getList(serviceConf.getString("attributeslist")," ");
        this.showOpAttributes = serviceConf.getBoolean("showopattributes",false);
        this.anonymousBind = serviceConf.getBoolean("anonymousbind",false);
        this.securityAuthentication = serviceConf.getString("securityauthentication","simple");
        this.securityProtocol = serviceConf.getString("securityprotocol");
        this.socketFactory = serviceConf.getString("socketfactory");
        this.useCachedDirContexts = serviceConf.getBoolean("contextcache", false);

        this.jndiprovider = serviceConf.getString("jndiprovider",DEFAULT_CTX);
        this.saslclientpckgs = serviceConf.getString("saslclientpckgs");
        mainConnect(new LDAPURL(host,port,basedn));
        setInit(true);
    }

    /**
     * This is the late initialization method called by the
     * Turbine <code>Service</code> framework
     * @param conf The <code>ServletConfig</code>
     * @exception InitializationException if the service fails to initialize
     */
    public void init() throws InitializationException
    {
        while( !getInit() )
        {
            //Not yet...
            try
            {
                Thread.sleep( 500 );
            }
            catch (InterruptedException ie )
            {
                logger.error( ie );
            }
        }
    }

    /**
     * Repair Given Parameter Function
     *
     * Repair String read from config.
     *
     * @param value String to repair.
     * @return String Repaired String.
     */
    private String repair(String value)
    {
        value = value.replace('/', '=');
        value = value.replace('%', ',');
        return value;
    }

    /**
     * Tokenizer Wrapper Function
     *
     * Tokenize given string with given parameter.
     *
     * @param value String to repair.
     * @param separator separator
     * @return String Result.
     */
    private String[] getList(String value, String separator)
    {
        if(value == null) return null;

        StringTokenizer tokens = new StringTokenizer(value, separator);
        String at[] = new String[tokens.countTokens()];

        for(int i = 0; tokens.hasMoreTokens(); i++)
		{
            at[i] = tokens.nextToken();
        }

        return at;
    }

}