/*
 * Copyright 2002-2019 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.integration.config.xml;

import java.util.Collection;
import java.util.List;

import org.w3c.dom.Element;

import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedSet;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ConsumerEndpointFactoryBean;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;

/**
 * Base class parser for elements that create Message Endpoints.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Gary Russell
 * @author Artem Bilan
 */
public abstract class AbstractConsumerEndpointParser extends AbstractBeanDefinitionParser {

	protected static final String REF_ATTRIBUTE = "ref";

	protected static final String METHOD_ATTRIBUTE = "method";

	protected static final String EXPRESSION_ATTRIBUTE = "expression";

	@Override
	protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
			throws BeanDefinitionStoreException {

		String id = element.getAttribute(ID_ATTRIBUTE);
		if (!StringUtils.hasText(id)) {
			id = element.getAttribute("name");
		}
		if (!StringUtils.hasText(id)) {
			id = BeanDefinitionReaderUtils.generateBeanName(definition, parserContext.getRegistry(),
					parserContext.isNested());
		}
		return id;
	}

	/**
	 * Parse the MessageHandler.
	 *
	 * @param element The element.
	 * @param parserContext The parser context.
	 * @return The bean definition builder.
	 */
	protected abstract BeanDefinitionBuilder parseHandler(Element element, ParserContext parserContext);

	protected String getInputChannelAttributeName() {
		return "input-channel";
	}

	@Override
	protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
		BeanDefinitionBuilder handlerBuilder = parseHandler(element, parserContext);
		IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "output-channel",
				"outputChannelName");
		IntegrationNamespaceUtils.setValueIfAttributeDefined(handlerBuilder, element, "order");

		Element txElement = DomUtils.getChildElementByTagName(element, "transactional");
		Element adviceChainElement = DomUtils.getChildElementByTagName(element,
				IntegrationNamespaceUtils.REQUEST_HANDLER_ADVICE_CHAIN);

		@SuppressWarnings("rawtypes")
		ManagedList adviceChain = IntegrationNamespaceUtils.configureAdviceChain(adviceChainElement, txElement, true,
				handlerBuilder.getRawBeanDefinition(), parserContext);

		boolean hasAdviceChain = !CollectionUtils.isEmpty(adviceChain);
		if (hasAdviceChain) {
			handlerBuilder.addPropertyValue("adviceChain", adviceChain);
		}

		AbstractBeanDefinition handlerBeanDefinition = handlerBuilder.getBeanDefinition();
		handlerBeanDefinition.setResource(parserContext.getReaderContext().getResource());
		String elementDescription = IntegrationNamespaceUtils.createElementDescription(element);
		handlerBeanDefinition.setSource(elementDescription);
		String inputChannelAttributeName = getInputChannelAttributeName();
		boolean hasInputChannelAttribute = element.hasAttribute(inputChannelAttributeName);
		if (parserContext.isNested()) {
			if (hasInputChannelAttribute) {
				parserContext.getReaderContext().error("The '" + inputChannelAttributeName
						+ "' attribute isn't allowed for a nested (e.g. inside a <chain/>) endpoint element: "
						+ elementDescription + ".", element);
			}
			if (!replyChannelInChainAllowed(element)
					&& StringUtils.hasText(element.getAttribute("reply-channel"))) {
				parserContext.getReaderContext().error("The 'reply-channel' attribute isn't"
						+ " allowed for a nested (e.g. inside a <chain/>) outbound gateway element: "
						+ elementDescription + ".", element);
			}
			return handlerBeanDefinition;
		}
		else {
			if (!hasInputChannelAttribute) {
				parserContext.getReaderContext().error("The '" + inputChannelAttributeName
						+ "' attribute is required for the top-level endpoint element: "
						+ elementDescription + ".", element);
			}
		}

		BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ConsumerEndpointFactoryBean.class);

		if (hasAdviceChain) {
			builder.addPropertyValue("adviceChain", adviceChain);
		}

		String handlerBeanName =
				BeanDefinitionReaderUtils.generateBeanName(handlerBeanDefinition, parserContext.getRegistry());
		String[] handlerAlias = IntegrationNamespaceUtils.generateAlias(element);
		parserContext.registerBeanComponent(
				new BeanComponentDefinition(handlerBeanDefinition, handlerBeanName, handlerAlias));

		builder.addPropertyReference("handler", handlerBeanName);

		String inputChannelName = element.getAttribute(inputChannelAttributeName);

		if (!parserContext.getRegistry().containsBeanDefinition(inputChannelName)) {
			registerChannelForCreation(parserContext, inputChannelName);
		}
		IntegrationNamespaceUtils.checkAndConfigureFixedSubscriberChannel(element, parserContext, inputChannelName,
				handlerBeanName);

		builder.addPropertyValue("inputChannelName", inputChannelName);
		List<Element> pollerElementList = DomUtils.getChildElementsByTagName(element, "poller");
		poller(element, parserContext, builder, pollerElementList);
		IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP);
		IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE);
		IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.ROLE);

		AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
		String beanName = resolveId(element, beanDefinition, parserContext);
		parserContext.registerBeanComponent(new BeanComponentDefinition(beanDefinition, beanName));
		return null;
	}

	private void poller(Element element, ParserContext parserContext, BeanDefinitionBuilder builder,
			List<Element> pollerElementList) {
		if (!CollectionUtils.isEmpty(pollerElementList)) {
			if (pollerElementList.size() != 1) {
				parserContext.getReaderContext().error(
						"at most one poller element may be configured for an endpoint", element);
			}
			IntegrationNamespaceUtils.configurePollerMetadata(pollerElementList.get(0), builder, parserContext);
		}
	}

	private void registerChannelForCreation(ParserContext parserContext, String inputChannelName) {
		if (parserContext.getRegistry()
				.containsBeanDefinition(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME)) {

			BeanDefinition channelRegistry = parserContext.getRegistry().
					getBeanDefinition(IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME);
			ConstructorArgumentValues caValues = channelRegistry.getConstructorArgumentValues();
			ValueHolder vh = caValues.getArgumentValue(0, Collection.class);
			if (vh == null) { //although it should never happen if it does we can fix it
				caValues.addIndexedArgumentValue(0, new ManagedSet<String>());
			}

			@SuppressWarnings("unchecked")
			Collection<String> channelCandidateNames =
					(Collection<String>) caValues.getArgumentValue(0, Collection.class)
							.getValue(); // NOSONAR see comment above
			channelCandidateNames.add(inputChannelName); // NOSONAR
		}
		else {
			parserContext.getReaderContext().error("Failed to locate '" +
							IntegrationContextUtils.AUTO_CREATE_CHANNEL_CANDIDATES_BEAN_NAME + "'",
					parserContext.getRegistry());
		}
	}

	/**
	 * Override to allow 'reply-channel' within a chain, for components where it
	 * makes sense (e.g. enricher). Default is false for outbound gateways, else true.
	 * @param element the element.
	 * @return true to allow a reply channel attribute within a chain.
	 * @since 4.3
	 */
	protected boolean replyChannelInChainAllowed(Element element) {
		String localName = element.getLocalName();
		return localName == null || !localName.contains("outbound-gateway");
	}

}
