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

import org.springframework.beans.factory.BeanFactory;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.message.AdviceMessage;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.util.StringUtils;

/**
 * Used to advise {@link org.springframework.messaging.MessageHandler}s.
 * Two expressions 'onSuccessExpression' and 'onFailureExpression' are evaluated when
 * appropriate. If the evaluation returns a result, a message is sent to the onSuccessChannel
 * or onFailureChannel as appropriate; the message is an {@link AdviceMessage}
 * containing the evaluation result in its payload and the {@code inputMessage} property containing
 * the original message that was sent to the endpoint.
 * The failure expression is NOT evaluated if the success expression throws an exception.
 * <p>
 * When expressions are not configured, but channels are, the default expression is evaluated
 * just into a {@code payload} from the message.
 *
 * @author Gary Russell
 * @author Artem Bilan
 *
 * @since 2.2
 *
 */
public class ExpressionEvaluatingRequestHandlerAdvice extends AbstractRequestHandlerAdvice {

	private static final Expression DEFAULT_EXPRESSION = new FunctionExpression<Message<?>>(Message::getPayload);

	private final MessagingTemplate messagingTemplate = new MessagingTemplate();

	private Expression onSuccessExpression;

	private MessageChannel successChannel;

	private String successChannelName;

	private Expression onFailureExpression;

	private MessageChannel failureChannel;

	private String failureChannelName;

	private boolean trapException = false;

	private boolean returnFailureExpressionResult = false;

	private boolean propagateOnSuccessEvaluationFailures;

	private EvaluationContext evaluationContext;

	/**
	 * Set the expression to evaluate against the message after a successful
	 * handler invocation.
	 * Defaults to {@code payload}, if {@code successChannel} is configured.
	 * @param onSuccessExpression the SpEL expression.
	 * @since 4.3.7
	 */
	public void setOnSuccessExpressionString(String onSuccessExpression) {
		setOnSuccessExpression(EXPRESSION_PARSER.parseExpression(onSuccessExpression));
	}

	/**
	 * Set the expression to evaluate against the message after a successful
	 * handler invocation.
	 * Defaults to {@code payload}, if {@code successChannel} is configured.
	 * @param onSuccessExpression the SpEL expression.
	 * @since 5.0
	 */
	public void setOnSuccessExpression(@Nullable Expression onSuccessExpression) {
		this.onSuccessExpression = onSuccessExpression;
	}

	/**
	 * Set the expression to evaluate against the message after a successful
	 * handler invocation.
	 * @param onSuccessExpression the SpEL expression.
	 * @deprecated in favor of {@link #setOnSuccessExpression(Expression)}
	 */
	@Deprecated
	public void setExpressionOnSuccess(Expression onSuccessExpression) {
		setOnSuccessExpression(onSuccessExpression);
	}

	/**
	 * Set the expression to evaluate against the root message after a failed
	 * handler invocation. The exception is available as the variable {@code #exception}.
	 * Defaults to {@code payload}, if {@code failureChannel} is configured.
	 * @param onFailureExpression the SpEL expression.
	 * @since 4.3.7
	 */
	public void setOnFailureExpressionString(String onFailureExpression) {
		setOnFailureExpression(EXPRESSION_PARSER.parseExpression(onFailureExpression));
	}

	/**
	 * Set the expression to evaluate against the root message after a failed
	 * handler invocation. The exception is available as the variable {@code #exception}.
	 * Defaults to {@code payload}, if {@code failureChannel} is configured.
	 * @param onFailureExpression the SpEL expression.
	 * @since 5.0
	 */
	public void setOnFailureExpression(@Nullable Expression onFailureExpression) {
		this.onFailureExpression = onFailureExpression;
	}

	/**
	 * Set the expression to evaluate against the root message after a failed
	 * handler invocation. The exception is available as the variable {@code #exception}
	 * @param onFailureExpression the SpEL expression.
	 * @deprecated in favor of {@link #setOnFailureExpression(Expression)}
	 */
	@Deprecated
	public void setExpressionOnFailure(Expression onFailureExpression) {
		setOnFailureExpression(onFailureExpression);
	}

	/**
	 * Set the channel to which to send the {@link AdviceMessage} after evaluating the
	 * success expression.
	 * @param successChannel the channel.
	 */
	public void setSuccessChannel(MessageChannel successChannel) {
		this.successChannel = successChannel;
	}

	/**
	 * Set the channel name to which to send the {@link AdviceMessage} after evaluating
	 * the success expression.
	 * @param successChannelName the channel name.
	 * @since 4.3.7
	 */
	public void setSuccessChannelName(String successChannelName) {
		this.successChannelName = successChannelName;
	}

	/**
	 * Set the channel to which to send the {@link ErrorMessage} after evaluating the
	 * failure expression.
	 * @param failureChannel the channel.
	 */
	public void setFailureChannel(MessageChannel failureChannel) {
		this.failureChannel = failureChannel;
	}

	/**
	 * Set the channel name to which to send the {@link ErrorMessage} after evaluating the
	 * failure expression.
	 * @param failureChannelName the channel name.
	 * @since 4.3.7
	 */
	public void setFailureChannelName(String failureChannelName) {
		this.failureChannelName = failureChannelName;
	}

	/**
	 * If true, any exception will be caught and null returned.
	 * Default false.
	 * @param trapException true to trap Exceptions.
	 */
	public void setTrapException(boolean trapException) {
		this.trapException = trapException;
	}

	/**
	 * If true, the result of evaluating the onFailureExpression will
	 * be returned as the result of AbstractReplyProducingMessageHandler.handleRequestMessage(Message).
	 * @param returnFailureExpressionResult true to return the result of the evaluation.
	 */
	public void setReturnFailureExpressionResult(boolean returnFailureExpressionResult) {
		this.returnFailureExpressionResult = returnFailureExpressionResult;
	}

	/**
	 * If true and an onSuccess expression evaluation fails with an exception, the
	 * exception will be thrown to the caller. If false, the exception is caught. Default
	 * false. Ignored for onFailure expression evaluation - the original exception will be
	 * propagated (unless trapException is true).
	 * @param propagateOnSuccessEvaluationFailures The
	 * propagateOnSuccessEvaluationFailures to set.
	 */
	public void setPropagateEvaluationFailures(boolean propagateOnSuccessEvaluationFailures) {
		this.propagateOnSuccessEvaluationFailures = propagateOnSuccessEvaluationFailures;
	}

	@Override
	protected void onInit() {
		super.onInit();
		BeanFactory beanFactory = getBeanFactory();
		if (beanFactory != null) {
			this.messagingTemplate.setBeanFactory(beanFactory);
		}

		if (this.onSuccessExpression == null
				&& (this.successChannel != null || StringUtils.hasText(this.successChannelName))) {

			this.onSuccessExpression = DEFAULT_EXPRESSION;
		}

		if (this.onFailureExpression == null
				&& (this.failureChannel != null || StringUtils.hasText(this.failureChannelName))) {

			this.onFailureExpression = DEFAULT_EXPRESSION;
		}
	}

	@Override
	protected Object doInvoke(ExecutionCallback callback, Object target, Message<?> message) {
		try {
			Object result = callback.execute();
			if (this.onSuccessExpression != null) {
				evaluateSuccessExpression(message);
			}
			return result;
		}
		catch (RuntimeException e) {
			Exception actualException = unwrapExceptionIfNecessary(e);
			if (this.onFailureExpression != null) {
				Object evalResult = evaluateFailureExpression(message, actualException);
				if (this.returnFailureExpressionResult) {
					return evalResult;
				}
			}
			if (!this.trapException) {
				if (e instanceof ThrowableHolderException) { // NOSONAR
					throw (ThrowableHolderException) e;
				}
				else {
					throw new ThrowableHolderException(actualException); // NOSONAR lost stack trace
				}
			}
			return null;
		}
	}

	private void evaluateSuccessExpression(Message<?> message) {
		Object evalResult;
		try {
			evalResult = this.onSuccessExpression.getValue(prepareEvaluationContextToUse(null), message);
		}
		catch (Exception e) {
			evalResult = e;
		}
		DestinationResolver<MessageChannel> channelResolver = getChannelResolver();
		if (this.successChannel == null && this.successChannelName != null && channelResolver != null) {
			this.successChannel = channelResolver.resolveDestination(this.successChannelName);
		}
		if (evalResult != null && this.successChannel != null) {
			AdviceMessage<?> resultMessage = new AdviceMessage<>(evalResult, message);
			this.messagingTemplate.send(this.successChannel, resultMessage);
		}
		if (evalResult instanceof Exception && this.propagateOnSuccessEvaluationFailures) {
			throw new ThrowableHolderException((Exception) evalResult);
		}
	}

	private Object evaluateFailureExpression(Message<?> message, Exception exception) {
		Object evalResult;
		try {
			evalResult = this.onFailureExpression.getValue(prepareEvaluationContextToUse(exception), message);
		}
		catch (Exception e) {
			evalResult = e;
			logger.error("Failure expression evaluation failed for " + message + ": " + e.getMessage());
		}
		DestinationResolver<MessageChannel> channelResolver = getChannelResolver();
		if (this.failureChannel == null && this.failureChannelName != null && channelResolver != null) {
			this.failureChannel = channelResolver.resolveDestination(this.failureChannelName);
		}
		if (evalResult != null && this.failureChannel != null) {
			MessagingException messagingException =
					new MessageHandlingExpressionEvaluatingAdviceException(message, "Handler Failed",
							unwrapThrowableIfNecessary(exception), evalResult);
			ErrorMessage errorMessage = new ErrorMessage(messagingException);
			this.messagingTemplate.send(this.failureChannel, errorMessage);
		}
		return evalResult;
	}

	protected StandardEvaluationContext createEvaluationContext() {
		return ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
	}

	/**
	 * If we don't need variables (i.e., exception is null)
	 * we can use a singleton context; otherwise we need a new one each time.
	 * @param exception the {@link Exception} to use in the context.
	 * @return The context.
	 */
	private EvaluationContext prepareEvaluationContextToUse(Exception exception) {
		EvaluationContext evaluationContextToUse;
		if (exception != null) {
			evaluationContextToUse = createEvaluationContext();
			evaluationContextToUse.setVariable("exception", exception);
		}
		else {
			if (this.evaluationContext == null) {
				this.evaluationContext = createEvaluationContext();
			}
			evaluationContextToUse = this.evaluationContext;
		}
		return evaluationContextToUse;
	}

	public static class MessageHandlingExpressionEvaluatingAdviceException extends MessagingException {

		private static final long serialVersionUID = 1L;

		private final Object evaluationResult;

		public MessageHandlingExpressionEvaluatingAdviceException(Message<?> message, String description,
				Throwable cause, Object evaluationResult) {

			super(message, description, cause);
			this.evaluationResult = evaluationResult;
		}

		public Object getEvaluationResult() {
			return this.evaluationResult;
		}

	}

}
