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

import java.util.List;

import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.NamespaceHandlerResolver;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Registers the central ProviderManager used by the namespace configuration, and allows
 * the configuration of an alias, allowing users to reference it in their beans and
 * clearly see where the name is coming from.
 *
 * @author Luke Taylor
 */
public class AuthenticationManagerBeanDefinitionParser implements BeanDefinitionParser {
	private static final String ATT_ALIAS = "alias";
	private static final String ATT_REF = "ref";
	private static final String ATT_ERASE_CREDENTIALS = "erase-credentials";

	public BeanDefinition parse(Element element, ParserContext pc) {
		String id = element.getAttribute("id");

		if (!StringUtils.hasText(id)) {
			if (pc.getRegistry().containsBeanDefinition(BeanIds.AUTHENTICATION_MANAGER)) {
				pc.getReaderContext().warning(
						"Overriding globally registered AuthenticationManager",
						pc.extractSource(element));
			}
			id = BeanIds.AUTHENTICATION_MANAGER;
		}
		pc.pushContainingComponent(new CompositeComponentDefinition(element.getTagName(),
				pc.extractSource(element)));

		BeanDefinitionBuilder providerManagerBldr = BeanDefinitionBuilder
				.rootBeanDefinition(ProviderManager.class);

		String alias = element.getAttribute(ATT_ALIAS);

		List<BeanMetadataElement> providers = new ManagedList<>();
		NamespaceHandlerResolver resolver = pc.getReaderContext()
				.getNamespaceHandlerResolver();

		NodeList children = element.getChildNodes();

		for (int i = 0; i < children.getLength(); i++) {
			Node node = children.item(i);
			if (node instanceof Element) {
				Element providerElt = (Element) node;
				if (StringUtils.hasText(providerElt.getAttribute(ATT_REF))) {
					if (providerElt.getAttributes().getLength() > 1) {
						pc.getReaderContext().error(
								"authentication-provider element cannot be used with other attributes "
										+ "when using 'ref' attribute",
								pc.extractSource(element));
					}
					NodeList providerChildren = providerElt.getChildNodes();
					for (int j = 0; j < providerChildren.getLength(); j++) {
						if (providerChildren.item(j) instanceof Element) {
							pc.getReaderContext().error(
									"authentication-provider element cannot have child elements when used "
											+ "with 'ref' attribute",
									pc.extractSource(element));
						}
					}
					providers.add(new RuntimeBeanReference(providerElt
							.getAttribute(ATT_REF)));
				}
				else {
					BeanDefinition provider = resolver.resolve(
							providerElt.getNamespaceURI()).parse(providerElt, pc);
					Assert.notNull(provider, "Parser for " + providerElt.getNodeName()
							+ " returned a null bean definition");
					String providerId = pc.getReaderContext().generateBeanName(provider);
					pc.registerBeanComponent(new BeanComponentDefinition(provider,
							providerId));
					providers.add(new RuntimeBeanReference(providerId));
				}
			}
		}

		if (providers.isEmpty()) {
			providers.add(new RootBeanDefinition(NullAuthenticationProvider.class));
		}

		providerManagerBldr.addConstructorArgValue(providers);

		if ("false".equals(element.getAttribute(ATT_ERASE_CREDENTIALS))) {
			providerManagerBldr.addPropertyValue("eraseCredentialsAfterAuthentication",
					false);
		}

		// Add the default event publisher
		BeanDefinition publisher = new RootBeanDefinition(
				DefaultAuthenticationEventPublisher.class);
		String pubId = pc.getReaderContext().generateBeanName(publisher);
		pc.registerBeanComponent(new BeanComponentDefinition(publisher, pubId));
		providerManagerBldr.addPropertyReference("authenticationEventPublisher", pubId);

		pc.registerBeanComponent(new BeanComponentDefinition(providerManagerBldr
				.getBeanDefinition(), id));

		if (StringUtils.hasText(alias)) {
			pc.getRegistry().registerAlias(id, alias);
			pc.getReaderContext().fireAliasRegistered(id, alias,
					pc.extractSource(element));
		}
		if (!BeanIds.AUTHENTICATION_MANAGER.equals(id)) {
			pc.getRegistry().registerAlias(id, BeanIds.AUTHENTICATION_MANAGER);
			pc.getReaderContext().fireAliasRegistered(id, BeanIds.AUTHENTICATION_MANAGER,
					pc.extractSource(element));
		}

		pc.popAndRegisterContainingComponent();

		return null;
	}

	/**
	 * Provider which doesn't provide any service. Only used to prevent a configuration
	 * exception if the provider list is empty (usually because a child ProviderManager
	 * from the &lt;http&gt; namespace, such as OpenID, is expected to handle the
	 * request).
	 */
	public static final class NullAuthenticationProvider implements
			AuthenticationProvider {
		public Authentication authenticate(Authentication authentication)
				throws AuthenticationException {
			return null;
		}

		public boolean supports(Class<?> authentication) {
			return false;
		}
	}
}
