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

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.RequestEntity;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.MapBindingResult;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.Controller;

/**
 * Inbound HTTP endpoint that implements Spring's {@link Controller} interface to be used with a DispatcherServlet front
 * controller.
 * <p>
 * The {@link #setViewName(String) viewName} will be passed into the ModelAndView return value.
 * <p>
 * This endpoint will have request/reply behavior by default. That can be overridden by passing <code>false</code> to
 * the constructor. In the request/reply case, the core map will be passed to the view, and it will contain either the
 * reply Message or payload depending on the value of {@link #setExtractReplyPayload(boolean)}
 * (true by default, meaning just the payload).
 * The corresponding key in the map is determined by the {@link #replyKey} property (with a default of "reply").
 *
 * @author Mark Fisher
 * @author Gary Russell
 * @author Artem Bilan
 *
 * @since 2.0
 */
public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSupport implements Controller {

	/**
	 * The View model key for the error code.
	 */
	public static final String DEFAULT_ERROR_CODE = "spring.integration.http.handler.error";

	/**
	 * The View model key for reply.
	 */
	public static final String DEFAULT_REPLY_KEY = "reply";

	/**
	 * The View model key for errors.
	 */
	public static final String DEFAULT_ERRORS_KEY = "errors";

	private Expression viewExpression;

	private StandardEvaluationContext evaluationContext;

	private String replyKey = DEFAULT_REPLY_KEY;

	private String errorsKey = DEFAULT_ERRORS_KEY;

	private String errorCode = DEFAULT_ERROR_CODE;

	public HttpRequestHandlingController() {
		this(true);
	}

	public HttpRequestHandlingController(boolean expectReply) {
		super(expectReply);
	}

	/**
	 * Specify the view name.
	 * @param viewName The view name.
	 */
	public void setViewName(String viewName) {
		Assert.isTrue(StringUtils.hasText(viewName), "View name must contain text");
		this.viewExpression = new LiteralExpression(viewName);
	}

	/**
	 * Specify the key to be used when adding the reply Message or payload to the core map (will be payload only unless
	 * the value of {@link HttpRequestHandlingController#setExtractReplyPayload(boolean)} is <code>false</code>). The
	 * default key is "reply".
	 * @param replyKey The reply key.
	 */
	public void setReplyKey(String replyKey) {
		this.replyKey = (replyKey != null) ? replyKey : DEFAULT_REPLY_KEY;
	}

	/**
	 * The key used to expose {@link org.springframework.validation.Errors} in the core,
	 * in the case that message handling fails. Defaults to "errors".
	 * @param errorsKey The key value to set.
	 */
	public void setErrorsKey(String errorsKey) {
		this.errorsKey = errorsKey;
	}

	/**
	 * The error code to use to signal an error in the message handling. In the case of an error this code will be
	 * provided in an object error to be optionally translated in the standard MVC way using a
	 * {@link org.springframework.context.MessageSource}.
	 * The default value is <code>spring.integration.http.handler.error</code>. Three arguments are provided: the
	 * exception, its message and its stack trace as a String.
	 * @param errorCode The error code to set.
	 */
	public void setErrorCode(String errorCode) {
		this.errorCode = errorCode;
	}

	/**
	 * Specifies a SpEL expression to evaluate in order to generate the view name.
	 * The EvaluationContext will be populated with the reply message as the root object,
	 * @param viewExpression The view expression.
	 */
	public void setViewExpression(Expression viewExpression) {
		this.viewExpression = viewExpression;
	}

	@Override
	protected void onInit() {
		super.onInit();
		this.evaluationContext = this.createEvaluationContext();
	}

	/**
	 * Handles the HTTP request by generating a Message and sending it to the request channel. If this gateway's
	 * 'expectReply' property is true, it will also generate a response from the reply Message once received.
	 */
	@Override
	public final ModelAndView handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
		ModelAndView modelAndView = new ModelAndView();
		try {
			ServletServerHttpRequest request = prepareRequest(servletRequest);

			RequestEntity<Object> httpEntity = prepareRequestEntity(request);

			Message<?> replyMessage = doHandleRequest(servletRequest, httpEntity);
			ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);
			if (replyMessage != null) {
				Object reply = setupResponseAndConvertReply(response, replyMessage);
				response.close();
				modelAndView.addObject(this.replyKey, reply);
			}
			else {
				setStatusCodeIfNeeded(response, httpEntity);
			}

			if (this.viewExpression != null) {
				Object view;
				if (replyMessage != null) {
					view = this.viewExpression.getValue(this.evaluationContext, replyMessage);
				}
				else {
					view = this.viewExpression.getValue(this.evaluationContext);
				}
				if (view instanceof View) {
					modelAndView.setView((View) view);
				}
				else if (view instanceof String) {
					modelAndView.setViewName((String) view);
				}
				else {
					throw new IllegalStateException("view expression must resolve to a View or String");
				}
			}
		}
		catch (Exception e) {
			MapBindingResult errors = new MapBindingResult(new HashMap<String, Object>(), "dummy");
			PrintWriter stackTrace = new PrintWriter(new StringWriter());
			e.printStackTrace(stackTrace);
			errors.reject(this.errorCode, new Object[] { e, e.getMessage(), stackTrace.toString() },
					"A Spring Integration handler raised an exception while handling an HTTP request.  " +
							"The exception is of type " + e.getClass() + " and it has a message: (" +
							e.getMessage() + ")");
			modelAndView.addObject(this.errorsKey, errors);
		}
		return modelAndView;
	}

}
