/*
 * 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
 *
 *      https://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.config.ldap;

import org.w3c.dom.Element;

import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.authentication.AbstractUserDetailsServiceBeanDefinitionParser;
import org.springframework.util.StringUtils;

/**
 * @author Luke Taylor
 * @since 2.0
 */
public class LdapUserServiceBeanDefinitionParser extends AbstractUserDetailsServiceBeanDefinitionParser {

	public static final String ATT_SERVER = "server-ref";

	public static final String ATT_USER_SEARCH_FILTER = "user-search-filter";

	public static final String ATT_USER_SEARCH_BASE = "user-search-base";

	public static final String DEF_USER_SEARCH_BASE = "";

	public static final String ATT_GROUP_SEARCH_FILTER = "group-search-filter";

	public static final String ATT_GROUP_SEARCH_BASE = "group-search-base";

	public static final String ATT_GROUP_ROLE_ATTRIBUTE = "group-role-attribute";

	public static final String DEF_GROUP_SEARCH_FILTER = "(uniqueMember={0})";

	public static final String DEF_GROUP_SEARCH_BASE = "";

	static final String ATT_ROLE_PREFIX = "role-prefix";

	static final String ATT_USER_CLASS = "user-details-class";

	static final String ATT_USER_CONTEXT_MAPPER_REF = "user-context-mapper-ref";

	static final String OPT_PERSON = "person";

	static final String OPT_INETORGPERSON = "inetOrgPerson";

	public static final String LDAP_SEARCH_CLASS = "org.springframework.security.ldap.search.FilterBasedLdapUserSearch";

	public static final String PERSON_MAPPER_CLASS = "org.springframework.security.ldap.userdetails.PersonContextMapper";

	public static final String INET_ORG_PERSON_MAPPER_CLASS = "org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper";

	public static final String LDAP_USER_MAPPER_CLASS = "org.springframework.security.ldap.userdetails.LdapUserDetailsMapper";

	public static final String LDAP_AUTHORITIES_POPULATOR_CLASS = "org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator";

	@Override
	protected String getBeanClassName(Element element) {
		return "org.springframework.security.ldap.userdetails.LdapUserDetailsService";
	}

	@Override
	protected void doParse(Element elt, ParserContext parserContext, BeanDefinitionBuilder builder) {
		if (!StringUtils.hasText(elt.getAttribute(ATT_USER_SEARCH_FILTER))) {
			parserContext.getReaderContext().error("User search filter must be supplied", elt);
		}
		builder.addConstructorArgValue(parseSearchBean(elt, parserContext));
		builder.getRawBeanDefinition().setSource(parserContext.extractSource(elt));
		builder.addConstructorArgValue(parseAuthoritiesPopulator(elt, parserContext));
		builder.addPropertyValue("userDetailsMapper", parseUserDetailsClassOrUserMapperRef(elt, parserContext));
	}

	static RootBeanDefinition parseSearchBean(Element elt, ParserContext parserContext) {
		String userSearchFilter = elt.getAttribute(ATT_USER_SEARCH_FILTER);
		String userSearchBase = elt.getAttribute(ATT_USER_SEARCH_BASE);
		Object source = parserContext.extractSource(elt);
		if (StringUtils.hasText(userSearchBase)) {
			if (!StringUtils.hasText(userSearchFilter)) {
				parserContext.getReaderContext()
						.error(ATT_USER_SEARCH_BASE + " cannot be used without a " + ATT_USER_SEARCH_FILTER, source);
			}
		}
		else {
			userSearchBase = DEF_USER_SEARCH_BASE;
		}
		if (!StringUtils.hasText(userSearchFilter)) {
			return null;
		}
		BeanDefinitionBuilder searchBuilder = BeanDefinitionBuilder.rootBeanDefinition(LDAP_SEARCH_CLASS);
		searchBuilder.getRawBeanDefinition().setSource(source);
		searchBuilder.addConstructorArgValue(userSearchBase);
		searchBuilder.addConstructorArgValue(userSearchFilter);
		searchBuilder.addConstructorArgValue(parseServerReference(elt, parserContext));
		return (RootBeanDefinition) searchBuilder.getBeanDefinition();
	}

	static RuntimeBeanReference parseServerReference(Element elt, ParserContext parserContext) {
		String server = elt.getAttribute(ATT_SERVER);
		boolean requiresDefaultName = false;
		if (!StringUtils.hasText(server)) {
			server = BeanIds.CONTEXT_SOURCE;
			requiresDefaultName = true;
		}
		RuntimeBeanReference contextSource = new RuntimeBeanReference(server);
		contextSource.setSource(parserContext.extractSource(elt));
		registerPostProcessorIfNecessary(parserContext.getRegistry(), requiresDefaultName);
		return contextSource;
	}

	private static void registerPostProcessorIfNecessary(BeanDefinitionRegistry registry, boolean defaultNameRequired) {
		if (registry.containsBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR)) {
			if (defaultNameRequired) {
				BeanDefinition bd = registry.getBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR);
				bd.getPropertyValues().addPropertyValue("defaultNameRequired", defaultNameRequired);
			}
			return;
		}
		BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(ContextSourceSettingPostProcessor.class);
		bdb.addPropertyValue("defaultNameRequired", defaultNameRequired);
		registry.registerBeanDefinition(BeanIds.CONTEXT_SOURCE_SETTING_POST_PROCESSOR, bdb.getBeanDefinition());
	}

	static BeanMetadataElement parseUserDetailsClassOrUserMapperRef(Element elt, ParserContext parserContext) {
		String userDetailsClass = elt.getAttribute(ATT_USER_CLASS);
		String userMapperRef = elt.getAttribute(ATT_USER_CONTEXT_MAPPER_REF);
		if (StringUtils.hasText(userDetailsClass) && StringUtils.hasText(userMapperRef)) {
			parserContext.getReaderContext().error("Attributes " + ATT_USER_CLASS + " and "
					+ ATT_USER_CONTEXT_MAPPER_REF + " cannot be used together.", parserContext.extractSource(elt));
		}
		if (StringUtils.hasText(userMapperRef)) {
			return new RuntimeBeanReference(userMapperRef);
		}
		RootBeanDefinition mapper = getMapper(userDetailsClass);
		mapper.setSource(parserContext.extractSource(elt));
		return mapper;
	}

	private static RootBeanDefinition getMapper(String userDetailsClass) {
		if (OPT_PERSON.equals(userDetailsClass)) {
			return new RootBeanDefinition(PERSON_MAPPER_CLASS, null, null);
		}
		if (OPT_INETORGPERSON.equals(userDetailsClass)) {
			return new RootBeanDefinition(INET_ORG_PERSON_MAPPER_CLASS, null, null);
		}
		return new RootBeanDefinition(LDAP_USER_MAPPER_CLASS, null, null);
	}

	static RootBeanDefinition parseAuthoritiesPopulator(Element elt, ParserContext parserContext) {
		String groupSearchFilter = elt.getAttribute(ATT_GROUP_SEARCH_FILTER);
		String groupSearchBase = elt.getAttribute(ATT_GROUP_SEARCH_BASE);
		String groupRoleAttribute = elt.getAttribute(ATT_GROUP_ROLE_ATTRIBUTE);
		String rolePrefix = elt.getAttribute(ATT_ROLE_PREFIX);
		if (!StringUtils.hasText(groupSearchFilter)) {
			groupSearchFilter = DEF_GROUP_SEARCH_FILTER;
		}
		if (!StringUtils.hasText(groupSearchBase)) {
			groupSearchBase = DEF_GROUP_SEARCH_BASE;
		}
		BeanDefinitionBuilder populator = BeanDefinitionBuilder.rootBeanDefinition(LDAP_AUTHORITIES_POPULATOR_CLASS);
		populator.getRawBeanDefinition().setSource(parserContext.extractSource(elt));
		populator.addConstructorArgValue(parseServerReference(elt, parserContext));
		populator.addConstructorArgValue(groupSearchBase);
		populator.addPropertyValue("groupSearchFilter", groupSearchFilter);
		populator.addPropertyValue("searchSubtree", Boolean.TRUE);
		if (StringUtils.hasText(rolePrefix)) {
			if ("none".equals(rolePrefix)) {
				rolePrefix = "";
			}
			populator.addPropertyValue("rolePrefix", rolePrefix);
		}
		if (StringUtils.hasLength(groupRoleAttribute)) {
			populator.addPropertyValue("groupRoleAttribute", groupRoleAttribute);
		}
		return (RootBeanDefinition) populator.getBeanDefinition();
	}

}
