/*
 * Copyright 2006-2023 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.batch.core.configuration.xml;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.springframework.batch.core.listener.StepListenerMetaData;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;

/**
 * Internal parser for the &lt;step/&gt; elements inside a job. A step element references
 * a bean definition for a {@link org.springframework.batch.core.Step} and goes on to
 * (optionally) list a set of transitions from that step to others with &lt;next
 * on="pattern" to="stepName"/&gt;. Used by the {@link JobParser}.
 *
 * @author Dave Syer
 * @author Thomas Risberg
 * @author Josh Long
 * @author Mahmoud Ben Hassine
 * @see JobParser
 * @since 2.0
 */
public abstract class AbstractStepParser {

	/**
	 * The ID attribute for the step parser.
	 */
	protected static final String ID_ATTR = "id";

	private static final String PARENT_ATTR = "parent";

	private static final String REF_ATTR = "ref";

	private static final String ALLOW_START_ATTR = "allow-start-if-complete";

	private static final String TASKLET_ELE = "tasklet";

	private static final String PARTITION_ELE = "partition";

	private static final String JOB_ELE = "job";

	private static final String JOB_PARAMS_EXTRACTOR_ATTR = "job-parameters-extractor";

	private static final String JOB_LAUNCHER_ATTR = "job-launcher";

	private static final String STEP_ATTR = "step";

	private static final String STEP_ELE = STEP_ATTR;

	private static final String PARTITIONER_ATTR = "partitioner";

	private static final String AGGREGATOR_ATTR = "aggregator";

	private static final String HANDLER_ATTR = "handler";

	private static final String HANDLER_ELE = "handler";

	private static final String TASK_EXECUTOR_ATTR = "task-executor";

	private static final String GRID_SIZE_ATTR = "grid-size";

	private static final String FLOW_ELE = "flow";

	private static final String JOB_REPO_ATTR = "job-repository";

	private static final StepListenerParser stepListenerParser = new StepListenerParser(
			StepListenerMetaData.stepExecutionListenerMetaData());

	/**
	 * @param stepElement The &lt;step/&gt; element
	 * @param parserContext instance of {@link ParserContext}.
	 * @param jobFactoryRef the reference to the {@link JobParserJobFactoryBean} from the
	 * enclosing tag. Use 'null' if unknown.
	 * @return {@link AbstractBeanDefinition} for the stepElement.
	 */
	protected AbstractBeanDefinition parseStep(Element stepElement, ParserContext parserContext, String jobFactoryRef) {

		BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
		AbstractBeanDefinition bd = builder.getRawBeanDefinition();

		// look at all nested elements
		NodeList children = stepElement.getChildNodes();

		for (int i = 0; i < children.getLength(); i++) {
			Node nd = children.item(i);

			if (nd instanceof Element nestedElement) {
				String name = nestedElement.getLocalName();

				if (TASKLET_ELE.equals(name)) {
					boolean stepUnderspecified = CoreNamespaceUtils.isUnderspecified(stepElement);
					new TaskletParser().parseTasklet(stepElement, nestedElement, bd, parserContext, stepUnderspecified);
				}
				else if (FLOW_ELE.equals(name)) {
					boolean stepUnderspecified = CoreNamespaceUtils.isUnderspecified(stepElement);
					parseFlow(stepElement, nestedElement, bd, parserContext, stepUnderspecified);
				}
				else if (PARTITION_ELE.equals(name)) {
					boolean stepUnderspecified = CoreNamespaceUtils.isUnderspecified(stepElement);
					parsePartition(stepElement, nestedElement, bd, parserContext, stepUnderspecified, jobFactoryRef);
				}
				else if (JOB_ELE.equals(name)) {
					boolean stepUnderspecified = CoreNamespaceUtils.isUnderspecified(stepElement);
					parseJob(stepElement, nestedElement, bd, parserContext, stepUnderspecified);
				}
				else if ("description".equals(name)) {
					bd.setDescription(nestedElement.getTextContent());
				}

				// nested bean reference/declaration
				else {
					String ns = nestedElement.getNamespaceURI();
					Object value = null;
					boolean skip = false;

					// Spring NS
					if ((ns == null && name.equals(BeanDefinitionParserDelegate.BEAN_ELEMENT))
							|| ns.equals(BeanDefinitionParserDelegate.BEANS_NAMESPACE_URI)) {
						BeanDefinitionHolder holder = parserContext.getDelegate()
							.parseBeanDefinitionElement(nestedElement);
						value = parserContext.getDelegate().decorateBeanDefinitionIfRequired(nestedElement, holder);
					}
					// Spring Batch transitions
					else if (ns.equals("http://www.springframework.org/schema/batch")) {
						// don't parse
						skip = true;
					}
					// Custom NS
					else {
						value = parserContext.getDelegate().parseCustomElement(nestedElement);
					}

					if (!skip) {
						bd.setBeanClass(StepParserStepFactoryBean.class);
						bd.setAttribute("isNamespaceStep", true);
						builder.addPropertyValue("tasklet", value);
					}
				}
			}
		}

		String parentRef = stepElement.getAttribute(PARENT_ATTR);
		if (StringUtils.hasText(parentRef)) {
			bd.setParentName(parentRef);
		}

		String isAbstract = stepElement.getAttribute("abstract");
		if (StringUtils.hasText(isAbstract)) {
			bd.setAbstract(Boolean.parseBoolean(isAbstract));
		}

		String jobRepositoryRef = stepElement.getAttribute(JOB_REPO_ATTR);
		if (StringUtils.hasText(jobRepositoryRef)) {
			builder.addPropertyReference("jobRepository", jobRepositoryRef);
		}

		if (StringUtils.hasText(jobFactoryRef)) {
			bd.setAttribute("jobParserJobFactoryBeanRef", jobFactoryRef);
		}

		// add the allow parser here
		String isAllowStart = stepElement.getAttribute(ALLOW_START_ATTR);
		if (StringUtils.hasText(isAllowStart)) {
			// check if the value is already set from an inner element
			if (!bd.getPropertyValues().contains("allowStartIfComplete")) {
				// set the value as a property
				bd.getPropertyValues().add("allowStartIfComplete", Boolean.valueOf(isAllowStart));
			} // end if
		}

		stepListenerParser.handleListenersElement(stepElement, bd, parserContext);
		return bd;
	}

	private void parsePartition(Element stepElement, Element partitionElement, AbstractBeanDefinition bd,
			ParserContext parserContext, boolean stepUnderspecified, String jobFactoryRef) {

		bd.setBeanClass(StepParserStepFactoryBean.class);
		bd.setAttribute("isNamespaceStep", true);
		String stepRef = partitionElement.getAttribute(STEP_ATTR);
		String partitionerRef = partitionElement.getAttribute(PARTITIONER_ATTR);
		String aggregatorRef = partitionElement.getAttribute(AGGREGATOR_ATTR);
		String handlerRef = partitionElement.getAttribute(HANDLER_ATTR);

		if (!StringUtils.hasText(partitionerRef)) {
			parserContext.getReaderContext().error("You must specify a partitioner", partitionElement);
			return;
		}

		MutablePropertyValues propertyValues = bd.getPropertyValues();

		propertyValues.addPropertyValue("partitioner", new RuntimeBeanReference(partitionerRef));
		if (StringUtils.hasText(aggregatorRef)) {
			propertyValues.addPropertyValue("stepExecutionAggregator", new RuntimeBeanReference(aggregatorRef));
		}

		boolean customHandler = false;
		if (!StringUtils.hasText(handlerRef)) {
			Element handlerElement = DomUtils.getChildElementByTagName(partitionElement, HANDLER_ELE);
			if (handlerElement != null) {
				String taskExecutorRef = handlerElement.getAttribute(TASK_EXECUTOR_ATTR);
				if (StringUtils.hasText(taskExecutorRef)) {
					propertyValues.addPropertyValue("taskExecutor", new RuntimeBeanReference(taskExecutorRef));
				}
				String gridSize = handlerElement.getAttribute(GRID_SIZE_ATTR);
				if (StringUtils.hasText(gridSize)) {
					propertyValues.addPropertyValue("gridSize", new TypedStringValue(gridSize));
				}
			}
		}
		else {
			customHandler = true;
			BeanDefinition partitionHandler = BeanDefinitionBuilder.genericBeanDefinition().getRawBeanDefinition();
			partitionHandler.setParentName(handlerRef);
			propertyValues.addPropertyValue("partitionHandler", partitionHandler);
		}

		Element inlineStepElement = DomUtils.getChildElementByTagName(partitionElement, STEP_ELE);
		if (inlineStepElement == null && !StringUtils.hasText(stepRef) && !customHandler) {
			parserContext.getReaderContext().error("You must specify a step", partitionElement);
			return;
		}

		if (StringUtils.hasText(stepRef)) {
			propertyValues.addPropertyValue("step", new RuntimeBeanReference(stepRef));
		}
		else if (inlineStepElement != null) {
			AbstractBeanDefinition stepDefinition = parseStep(inlineStepElement, parserContext, jobFactoryRef);
			stepDefinition.getPropertyValues().addPropertyValue("name", stepElement.getAttribute(ID_ATTR));
			propertyValues.addPropertyValue("step", stepDefinition);
		}

	}

	private void parseJob(Element stepElement, Element jobElement, AbstractBeanDefinition bd,
			ParserContext parserContext, boolean stepUnderspecified) {

		bd.setBeanClass(StepParserStepFactoryBean.class);
		bd.setAttribute("isNamespaceStep", true);
		String jobRef = jobElement.getAttribute(REF_ATTR);

		if (!StringUtils.hasText(jobRef)) {
			parserContext.getReaderContext().error("You must specify a job", jobElement);
			return;
		}

		MutablePropertyValues propertyValues = bd.getPropertyValues();
		propertyValues.addPropertyValue("job", new RuntimeBeanReference(jobRef));

		String jobParametersExtractor = jobElement.getAttribute(JOB_PARAMS_EXTRACTOR_ATTR);
		String jobLauncher = jobElement.getAttribute(JOB_LAUNCHER_ATTR);

		if (StringUtils.hasText(jobParametersExtractor)) {
			propertyValues.addPropertyValue("jobParametersExtractor", new RuntimeBeanReference(jobParametersExtractor));
		}
		if (StringUtils.hasText(jobLauncher)) {
			propertyValues.addPropertyValue("jobLauncher", new RuntimeBeanReference(jobLauncher));
		}

	}

	private void parseFlow(Element stepElement, Element flowElement, AbstractBeanDefinition bd,
			ParserContext parserContext, boolean stepUnderspecified) {

		bd.setBeanClass(StepParserStepFactoryBean.class);
		bd.setAttribute("isNamespaceStep", true);
		String flowRef = flowElement.getAttribute(PARENT_ATTR);
		String idAttribute = stepElement.getAttribute(ID_ATTR);

		BeanDefinition flowDefinition = new GenericBeanDefinition();
		flowDefinition.setParentName(flowRef);
		MutablePropertyValues propertyValues = flowDefinition.getPropertyValues();
		if (StringUtils.hasText(idAttribute)) {
			propertyValues.addPropertyValue("name", idAttribute);
		}

		bd.getPropertyValues().addPropertyValue("flow", flowDefinition);

	}

}
