/*
 * 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.IOException;
import java.util.Collections;
import java.util.List;

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

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.CollectionUtils;
import org.springframework.web.HttpRequestHandler;

/**
 * Inbound Messaging Gateway that handles HTTP Requests. May be configured as a bean in the Application Context and
 * delegated to from a simple HttpRequestHandlerServlet in <code>web.xml</code> where the servlet and bean both have the
 * same name. If the {@link #isExpectReply()} property is set to true, a response can generated from a reply Message.
 * Otherwise, the gateway will play the role of a unidirectional Channel Adapter with a simple status-based response
 * (e.g. 200 OK).
 * <p>
 * The default supported request methods are GET and POST, but the list of values can be configured with the
 * {@link RequestMapping#getMethods()} property.
 * The payload generated from a GET request (or HEAD or OPTIONS if supported) will
 * be a {@link org.springframework.util.MultiValueMap} containing the parameter values.
 * For a request containing a body (e.g. a POST), the type of the payload is determined
 * by the {@link #setRequestPayloadTypeClass(Class)} request payload type}.
 * <p>
 * If the HTTP request is a multipart and a "multipartResolver" bean has been defined in the context, then it will be
 * converted by the
 * {@link org.springframework.integration.http.converter.MultipartAwareFormHttpMessageConverter} as long as the default
 * message converters have not
 * been overwritten (although providing a customized instance of the Multipart-aware converter is also an option).
 * <p>
 * By default a number of {@link HttpMessageConverter}s are already configured. The list can be overridden by calling
 * the {@link #setMessageConverters(List)} method.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Artem Bilan
 * @author Gary Russell
 *
 * @since 2.0
 */
public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndpointSupport
		implements HttpRequestHandler {

	private volatile boolean convertExceptions;


	public HttpRequestHandlingMessagingGateway() {
		this(true);
	}

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


	/**
	 * Flag to determine if conversion and writing out of message handling exceptions should be attempted (default
	 * false, in which case they will simply be re-thrown). If the flag is true and no message converter can convert the
	 * exception a new exception will be thrown.
	 * @param convertExceptions the flag to set
	 */
	public void setConvertExceptions(boolean convertExceptions) {
		this.convertExceptions = convertExceptions;
	}

	/**
	 * 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. That
	 * response will be written by the {@link HttpMessageConverter}s.
	 */
	@Override
	public final void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
			throws IOException {

		Object responseContent = null;
		Message<?> responseMessage;

		ServletServerHttpRequest request = prepareRequest(servletRequest);
		ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);

		RequestEntity<Object> httpEntity = prepareRequestEntity(request);

		try {
			responseMessage = doHandleRequest(servletRequest, httpEntity);
			if (responseMessage != null) {
				responseContent = setupResponseAndConvertReply(response, responseMessage);
			}
		}
		catch (Exception ex) {
			responseContent = handleExceptionInternal(ex);
		}

		if (responseContent != null) {

			if (responseContent instanceof HttpStatus) {
				response.setStatusCode((HttpStatus) responseContent);
			}
			else {
				if (responseContent instanceof ResponseEntity) {
					ResponseEntity<?> responseEntity = (ResponseEntity<?>) responseContent;
					responseContent = responseEntity.getBody();
					response.setStatusCode(responseEntity.getStatusCode());

					HttpHeaders outputHeaders = response.getHeaders();
					HttpHeaders entityHeaders = responseEntity.getHeaders();

					entityHeaders.entrySet()
							.stream()
							.filter(entry -> !outputHeaders.containsKey(entry.getKey()))
							.forEach(entry -> outputHeaders.put(entry.getKey(), entry.getValue()));
				}

				if (responseContent != null) {
					writeResponse(responseContent, response, request.getHeaders().getAccept());
				}
				else {
					response.flush();
				}
			}
		}
		else {
			setStatusCodeIfNeeded(response, httpEntity);
		}
	}

	private Object handleExceptionInternal(Exception ex) throws IOException {
		if (this.convertExceptions && isExpectReply()) {
			return ex;
		}
		else {
			if (ex instanceof IOException) {
				throw (IOException) ex;
			}
			else if (ex instanceof RuntimeException) {
				throw (RuntimeException) ex;
			}
			else {
				throw new MessagingException("error occurred handling HTTP request", ex);
			}
		}
	}

	private void writeResponse(Object content, ServletServerHttpResponse response, List<MediaType> acceptTypesArg)
			throws IOException {

		List<MediaType> acceptTypes = acceptTypesArg;
		if (CollectionUtils.isEmpty(acceptTypes)) {
			acceptTypes = Collections.singletonList(MediaType.ALL);
		}
		for (HttpMessageConverter<?> converter : getMessageConverters()) {
			for (MediaType acceptType : acceptTypes) {
				if (converter.canWrite(content.getClass(), acceptType)) {
					@SuppressWarnings("unchecked")
					HttpMessageConverter<Object> converterToUse = (HttpMessageConverter<Object>) converter;
					converterToUse.write(content, acceptType, response);
					return;
				}
			}
		}
		throw new MessagingException("Could not convert reply: no suitable HttpMessageConverter found for type ["
				+ content.getClass().getName() + "] and accept types [" + acceptTypes + "]");
	}

}
