/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.ldap;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.ldap.core.support.DirContextAuthenticationStrategy;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
import org.springframework.util.Assert;

/**
 * ContextSource implementation which uses Spring LDAP's <tt>LdapContextSource</tt> as a
 * base class. Used internally by the Spring Security LDAP namespace configuration.
 * <p>
 * From Spring Security 3.0, Spring LDAP 1.3 is used and the <tt>ContextSource</tt>
 * interface provides support for binding with a username and password. As a result,
 * Spring LDAP <tt>ContextSource</tt> implementations such as <tt>LdapContextSource</tt>
 * may be used directly with Spring Security.
 * <p>
 * Spring LDAP 1.3 doesn't have JVM-level LDAP connection pooling enabled by default. This
 * class sets the <tt>pooled</tt> property to true, but customizes the
 * {@link DirContextAuthenticationStrategy} used to disable pooling when the <tt>DN</tt>
 * doesn't match the <tt>userDn</tt> property. This prevents pooling for calls to
 * {@link #getContext(String, String)} to authenticate as specific users.
 *
 * @author Luke Taylor
 * @since 2.0
 */
public class DefaultSpringSecurityContextSource extends LdapContextSource {
	protected final Log logger = LogFactory.getLog(getClass());

	private String rootDn;

	/**
	 * Create and initialize an instance which will connect to the supplied LDAP URL. If
	 * you want to use more than one server for fail-over, rather use the
	 * {@link #DefaultSpringSecurityContextSource(List, String)} constructor.
	 *
	 * @param providerUrl an LDAP URL of the form
	 * <code>ldap://localhost:389/base_dn</code>
	 */
	public DefaultSpringSecurityContextSource(String providerUrl) {
		Assert.hasLength(providerUrl, "An LDAP connection URL must be supplied.");

		StringTokenizer st = new StringTokenizer(providerUrl);

		ArrayList<String> urls = new ArrayList<>();

		// Work out rootDn from the first URL and check that the other URLs (if any) match
		while (st.hasMoreTokens()) {
			String url = st.nextToken();
			String urlRootDn = LdapUtils.parseRootDnFromUrl(url);

			urls.add(url.substring(0, url.lastIndexOf(urlRootDn)));

			this.logger.info(" URL '" + url + "', root DN is '" + urlRootDn + "'");

			if (this.rootDn == null) {
				this.rootDn = urlRootDn;
			}
			else if (!this.rootDn.equals(urlRootDn)) {
				throw new IllegalArgumentException(
						"Root DNs must be the same when using multiple URLs");
			}
		}

		setUrls(urls.toArray(new String[urls.size()]));
		setBase(this.rootDn);
		setPooled(true);
		setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy() {
			@Override
			@SuppressWarnings("rawtypes")
			public void setupEnvironment(Hashtable env, String dn, String password) {
				super.setupEnvironment(env, dn, password);
				// Remove the pooling flag unless we are authenticating as the 'manager'
				// user.
				if (!DefaultSpringSecurityContextSource.this.userDn.equals(dn)
						&& env.containsKey(SUN_LDAP_POOLING_FLAG)) {
					DefaultSpringSecurityContextSource.this.logger
							.debug("Removing pooling flag for user " + dn);
					env.remove(SUN_LDAP_POOLING_FLAG);
				}
			}
		});
	}

	/**
	 * Create and initialize an instance which will connect of the LDAP Spring Security
	 * Context Source. It will connect to any of the provided LDAP server URLs.
	 *
	 * @param urls A list of string values which are LDAP server URLs. An example would be
	 * <code>ldap://ldap.company.com:389</code>. LDAPS URLs (SSL-secured) may be used as
	 * well, given that Spring Security is able to connect to the server. Note that these
	 * <b>URLs must not include the base DN</b>!
	 * @param baseDn The common Base DN for all provided servers, e.g.
	 *
	 * <pre>
	 * dc=company,dc=com
	 * </pre>
	 *
	 * .
	 */
	public DefaultSpringSecurityContextSource(List<String> urls, String baseDn) {
		this(buildProviderUrl(urls, baseDn));
	}

	/**
	 * Builds a Spring LDAP-compliant Provider URL string, i.e. a space-separated list of
	 * LDAP servers with their base DNs. As the base DN must be identical for all servers,
	 * it needs to be supplied only once.
	 *
	 * @param urls A list of string values which are LDAP server URLs. An example would be
	 *
	 * <pre>
	 * ldap://ldap.company.com:389
	 * </pre>
	 *
	 * . LDAPS URLs may be used as well, given that Spring Security is able to connect to
	 * the server.
	 * @param baseDn The common Base DN for all provided servers, e.g.
	 *
	 * <pre>
	 * dc=company,dc=com
	 * </pre>
	 *
	 * .
	 * @return A Spring Security/Spring LDAP-compliant Provider URL string.
	 */
	private static String buildProviderUrl(List<String> urls, String baseDn) {
		Assert.notNull(baseDn, "The Base DN for the LDAP server must not be null.");
		Assert.notEmpty(urls, "At least one LDAP server URL must be provided.");

		String trimmedBaseDn = baseDn.trim();
		StringBuilder providerUrl = new StringBuilder();

		for (String serverUrl : urls) {
			String trimmedUrl = serverUrl.trim();
			if ("".equals(trimmedUrl)) {
				continue;
			}

			providerUrl.append(trimmedUrl);
			if (!trimmedUrl.endsWith("/")) {
				providerUrl.append("/");
			}
			providerUrl.append(trimmedBaseDn);
			providerUrl.append(" ");
		}

		return providerUrl.toString();

	}

}
