/*
 * Copyright 2013 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.batch.core.jsr.configuration.support;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import javax.batch.api.BatchProperty;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.scope.StepScope;
import org.springframework.batch.core.scope.context.StepContext;
import org.springframework.batch.core.scope.context.StepSynchronizationManager;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.expression.StandardBeanExpressionResolver;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * <p>
 * {@link BeanPostProcessor} implementation used to inject JSR-352 String properties into batch artifact fields
 * that are marked with the {@link BatchProperty} annotation.
 * </p>
 *
 * @author Chris Schaefer
 * @author Michael Minella
 * @since 3.0
 */
@SuppressWarnings("unchecked")
public class BatchPropertyBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
	private static final String SCOPED_TARGET_BEAN_PREFIX = "scopedTarget.";
	private static final Log LOGGER = LogFactory.getLog(BatchPropertyBeanPostProcessor.class);
	private static final Set<Class<? extends Annotation>> REQUIRED_ANNOTATIONS = new HashSet<Class<? extends Annotation>>();

	private JsrExpressionParser jsrExpressionParser;
	private BatchPropertyContext batchPropertyContext;

	static {
		ClassLoader cl = BatchPropertyBeanPostProcessor.class.getClassLoader();

		try {
			REQUIRED_ANNOTATIONS.add((Class<? extends Annotation>) cl.loadClass("javax.inject.Inject"));
		} catch (ClassNotFoundException ex) {
			LOGGER.warn("javax.inject.Inject not found - @BatchProperty marked fields will not be processed.");
		}

		REQUIRED_ANNOTATIONS.add(BatchProperty.class);
	}

	@Override
	public Object postProcessBeforeInitialization(final Object artifact, String artifactName) throws BeansException {
		if (! BatchArtifact.isBatchArtifact(artifact)) {
			return artifact;
		}

		Properties artifactProperties = getArtifactProperties(artifactName);

		if (artifactProperties.isEmpty()) {
			return artifact;
		}

		injectBatchProperties(artifact, artifactProperties);

		return artifact;
	}

	@Override
	public Object postProcessAfterInitialization(Object artifact, String artifactName) throws BeansException {
		return artifact;
	}

	private Properties getArtifactProperties(String artifactName) {
		StepContext stepContext = StepSynchronizationManager.getContext();

		if (stepContext != null) {
			String originalArtifactName = artifactName;
			if(originalArtifactName.startsWith(SCOPED_TARGET_BEAN_PREFIX)) {
				originalArtifactName = artifactName.substring(SCOPED_TARGET_BEAN_PREFIX.length());
			}

			return batchPropertyContext.getStepArtifactProperties(stepContext.getStepName(), originalArtifactName);
		}

		return batchPropertyContext.getArtifactProperties(artifactName);
	}

	private void injectBatchProperties(final Object artifact, final Properties artifactProperties) {
		ReflectionUtils.doWithFields(artifact.getClass(), new ReflectionUtils.FieldCallback() {
			@Override
			public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
				if (isValidFieldModifier(field) && isAnnotated(field)) {
					boolean isAccessible = field.isAccessible();
					field.setAccessible(true);

					String batchProperty = getBatchPropertyFieldValue(field, artifactProperties);

					if (StringUtils.hasText(batchProperty)) {
						field.set(artifact, batchProperty);
					}

					field.setAccessible(isAccessible);
				}
			}
		});
	}

	private String getBatchPropertyFieldValue(Field field, Properties batchArtifactProperties) {
		BatchProperty batchProperty = field.getAnnotation(BatchProperty.class);

		if (!"".equals(batchProperty.name())) {
			return getBatchProperty(batchProperty.name(), batchArtifactProperties);
		}

		return getBatchProperty(field.getName(), batchArtifactProperties);
	}

	private String getBatchProperty(String propertyKey, Properties batchArtifactProperties) {
		if (batchArtifactProperties.containsKey(propertyKey)) {
			String propertyValue = (String) batchArtifactProperties.get(propertyKey);

			return jsrExpressionParser.parseExpression(propertyValue);
		}

		return null;
	}

	private boolean isAnnotated(Field field) {
		for (Class<? extends Annotation> annotation : REQUIRED_ANNOTATIONS) {
			if (!field.isAnnotationPresent(annotation)) {
				return false;
			}
		}

		return true;
	}

	private boolean isValidFieldModifier(Field field) {
		return !Modifier.isStatic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers());
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
			throw new IllegalArgumentException(
					"BatchPropertyBeanPostProcessor requires a ConfigurableListableBeanFactory");
		}

		ConfigurableListableBeanFactory configurableListableBeanFactory = (ConfigurableListableBeanFactory) beanFactory;

		BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableListableBeanFactory,
				configurableListableBeanFactory.getBean(StepScope.class));

		this.jsrExpressionParser = new JsrExpressionParser(new StandardBeanExpressionResolver(), beanExpressionContext);
	}

	@Autowired
	public void setBatchPropertyContext(BatchPropertyContext batchPropertyContext) {
		this.batchPropertyContext = batchPropertyContext;
	}
}
