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

import java.util.Properties;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.channel.ChannelResolverUtils;
import org.springframework.integration.support.context.NamedComponent;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Assert;
import org.springframework.util.IdGenerator;
import org.springframework.util.StringUtils;

/**
 * A base class that provides convenient access to the bean factory as
 * well as {@link TaskScheduler} and {@link ConversionService} instances.
 *
 * <p>This is intended to be used as a base class for internal framework
 * components whereas code built upon the integration framework should not
 * require tight coupling with the context but rather rely on standard
 * dependency injection.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Josh Long
 * @author Stefan Ferstl
 * @author Gary Russell
 * @author Artem Bilan
 */
public abstract class IntegrationObjectSupport implements BeanNameAware, NamedComponent,
		ApplicationContextAware, BeanFactoryAware, InitializingBean, ExpressionCapable {

	protected static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();

	private static final IdGenerator ID_GENERATOR = new AlternativeJdkIdGenerator();

	/**
	 * Logger that is available to subclasses
	 */
	protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR protected

	private final ConversionService defaultConversionService = DefaultConversionService.getSharedInstance();

	private DestinationResolver<MessageChannel> channelResolver;

	private String beanName;

	private String componentName;

	private BeanFactory beanFactory;

	private TaskScheduler taskScheduler;

	private Properties integrationProperties = IntegrationProperties.defaults();

	private ConversionService conversionService;

	private ApplicationContext applicationContext;

	private MessageBuilderFactory messageBuilderFactory;

	private Expression expression;

	private boolean initialized;

	@Override
	public final void setBeanName(String beanName) {
		this.beanName = beanName;
	}

	@Override
	public String getBeanName() {
		return this.beanName;
	}

	/**
	 * Will return the name of this component identified by {@link #componentName} field.
	 * If {@link #componentName} was not set this method will default to the 'beanName' of this component;
	 */
	@Override
	public String getComponentName() {
		return StringUtils.hasText(this.componentName) ? this.componentName : this.beanName;
	}

	/**
	 * Sets the name of this component.
	 * @param componentName The component name.
	 */
	public void setComponentName(String componentName) {
		this.componentName = componentName;
	}

	/**
	 * Subclasses may implement this method to provide component type information.
	 */
	@Override
	public String getComponentType() {
		return null;
	}

	public String getBeanDescription() {
		String description = null;
		Object source = null;

		if (this.beanFactory instanceof ConfigurableListableBeanFactory &&
				((ConfigurableListableBeanFactory) this.beanFactory).containsBeanDefinition(this.beanName)) {
			BeanDefinition beanDefinition =
					((ConfigurableListableBeanFactory) this.beanFactory).getBeanDefinition(this.beanName);
			description = beanDefinition.getResourceDescription();
			source = beanDefinition.getSource();
		}

		StringBuilder sb = new StringBuilder("bean '")
				.append(this.beanName).append("'");
		if (!this.beanName.equals(getComponentName())) {
			sb.append(" for component '").append(getComponentName()).append("'");
		}
		if (description != null) {
			sb.append("; defined in: '").append(description).append("'");
		}
		if (source != null) {
			sb.append("; from source: '").append(source).append("'");
		}
		return sb.toString();
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		Assert.notNull(beanFactory, "'beanFactory' must not be null");
		this.beanFactory = beanFactory;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		Assert.notNull(applicationContext, "'applicationContext' must not be null");
		this.applicationContext = applicationContext;
	}

	/**
	 * Specify the {@link DestinationResolver} strategy to use.
	 * The default is a BeanFactoryChannelResolver.
	 * @param channelResolver The channel resolver.
	 */
	public void setChannelResolver(DestinationResolver<MessageChannel> channelResolver) {
		Assert.notNull(channelResolver, "'channelResolver' must not be null");
		this.channelResolver = channelResolver;
	}

	@Override
	public Expression getExpression() {
		return this.expression;
	}

	/**
	 * For expression-based components, set the primary expression.
	 * @param expression the expression.
	 * @since 4.3
	 */
	public final void setPrimaryExpression(Expression expression) {
		this.expression = expression;
	}

	@Override
	public final void afterPropertiesSet() {
		this.integrationProperties = IntegrationContextUtils.getIntegrationProperties(this.beanFactory);
		if (this.messageBuilderFactory == null) {
			if (this.beanFactory != null) {
				this.messageBuilderFactory = IntegrationUtils.getMessageBuilderFactory(this.beanFactory);
			}
			else {
				this.messageBuilderFactory = new DefaultMessageBuilderFactory();
			}
		}
		onInit();
		this.initialized = true;
	}

	/**
	 * Subclasses may implement this for initialization logic.
	 */
	protected void onInit() {
	}

	/**
	 * Return the status of this component if it has been initialized already.
	 * @return the flag if this component has been initialized already.
	 */
	protected boolean isInitialized() {
		return this.initialized;
	}

	protected BeanFactory getBeanFactory() {
		return this.beanFactory;
	}

	/**
	 * Configure a {@link TaskScheduler} for those components which logic relies
	 * on the scheduled tasks.
	 * If not provided, falls back to the global {@code taskScheduler} bean
	 * in the application context, provided by the Spring Integration infrastructure.
	 * @param taskScheduler the {@link TaskScheduler} to use.
	 * @since 5.1.3
	 * @see #getTaskScheduler()
	 */
	public void setTaskScheduler(TaskScheduler taskScheduler) {
		Assert.notNull(taskScheduler, "taskScheduler must not be null");
		this.taskScheduler = taskScheduler;
	}

	protected TaskScheduler getTaskScheduler() {
		if (this.taskScheduler == null && this.beanFactory != null) {
			this.taskScheduler = IntegrationContextUtils.getTaskScheduler(this.beanFactory);
		}
		return this.taskScheduler;
	}

	protected DestinationResolver<MessageChannel> getChannelResolver() {
		if (this.channelResolver == null) {
			this.channelResolver = ChannelResolverUtils.getChannelResolver(this.beanFactory);
		}
		return this.channelResolver;
	}

	public ConversionService getConversionService() {
		if (this.conversionService == null && this.beanFactory != null) {
			this.conversionService = IntegrationUtils.getConversionService(this.beanFactory);
			if (this.conversionService == null && this.logger.isDebugEnabled()) {
				this.logger.debug("Unable to attempt conversion of Message payload types. Component '" +
						getComponentName() + "' has no explicit ConversionService reference, " +
						"and there is no 'integrationConversionService' bean within the context.");
			}
		}
		return this.conversionService;
	}

	protected void setConversionService(ConversionService conversionService) {
		this.conversionService = conversionService;
	}

	/**
	 * Returns the {@link ApplicationContext#getId()} if the
	 * {@link ApplicationContext} is available.
	 * @return The id, or null if there is no application context.
	 */
	public String getApplicationContextId() {
		return this.applicationContext == null ? null : this.applicationContext.getId();
	}

	/**
	 * @return the applicationContext
	 */
	protected ApplicationContext getApplicationContext() {
		return this.applicationContext;
	}

	/**
	 * @see IntegrationContextUtils#getIntegrationProperties(BeanFactory)
	 * @return The global integration properties.
	 */
	protected Properties getIntegrationProperties() {
		return this.integrationProperties;
	}


	protected MessageBuilderFactory getMessageBuilderFactory() {
		if (this.messageBuilderFactory == null) {
			this.messageBuilderFactory = new DefaultMessageBuilderFactory();
		}
		return this.messageBuilderFactory;
	}

	public void setMessageBuilderFactory(MessageBuilderFactory messageBuilderFactory) {
		this.messageBuilderFactory = messageBuilderFactory;
	}

	/**
	 * @param  key    Integration property.
	 * @param  tClass the class to convert a value of Integration property.
	 * @param <T> The expected type of the property.
	 * @return the value of the Integration property converted to the provide type.
	 */
	protected <T> T getIntegrationProperty(String key, Class<T> tClass) {
		return this.defaultConversionService.convert(this.integrationProperties.getProperty(key), tClass);
	}

	@Override
	public String toString() {
		return (this.beanName != null) ? getBeanDescription() : super.toString();
	}

	@SuppressWarnings("unchecked")
	@Nullable
	public static <T> T extractTypeIfPossible(@Nullable Object targetObject, Class<T> expectedType) {
		if (targetObject == null) {
			return null;
		}
		if (expectedType.isAssignableFrom(targetObject.getClass())) {
			return (T) targetObject;
		}
		else {
			return extractTypeIfPossible(AopProxyUtils.getSingletonTarget(targetObject), expectedType);
		}
	}

	public static UUID generateId() {
		return ID_GENERATOR.generateId();
	}

}
