/*
 * Copyright 2014-2015 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.data.rest.webmvc;

import java.lang.reflect.InvocationTargetException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.rest.core.RepositoryConstraintViolationException;
import org.springframework.data.rest.webmvc.support.ETagDoesntMatchException;
import org.springframework.data.rest.webmvc.support.ExceptionMessage;
import org.springframework.data.rest.webmvc.support.RepositoryConstraintViolationExceptionMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

/**
 * Exception handler for Spring Data REST controllers.
 * 
 * @author Thibaud Lepretre
 * @author Oliver Gierke
 */
@ControllerAdvice(basePackageClasses = RepositoryRestExceptionHandler.class)
public class RepositoryRestExceptionHandler {

	private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestExceptionHandler.class);

	private final MessageSourceAccessor messageSourceAccessor;

	/**
	 * Creates a new {@link RepositoryRestExceptionHandler} using the given {@link MessageSource}.
	 * 
	 * @param messageSource must not be {@literal null}.
	 */
	public RepositoryRestExceptionHandler(MessageSource messageSource) {

		Assert.notNull(messageSource, "MessageSource must not be null!");
		this.messageSourceAccessor = new MessageSourceAccessor(messageSource);
	}

	/**
	 * Handles {@link ResourceNotFoundException} by returning {@code 404 Not Found}.
	 * 
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler
	ResponseEntity<?> handleNotFound(ResourceNotFoundException o_O) {
		return notFound(new HttpHeaders());
	}

	/**
	 * Handles {@link HttpMessageNotReadableException} by returning {@code 400 Bad Request}.
	 * 
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler
	ResponseEntity<ExceptionMessage> handleNotReadable(HttpMessageNotReadableException o_O) {
		return badRequest(new HttpHeaders(), o_O);
	}

	/**
	 * Handle failures commonly thrown from code tries to read incoming data and convert or cast it to the right type by
	 * returning {@code 500 Internal Server Error} and the thrown exception marshalled into JSON.
	 *
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler({ InvocationTargetException.class, IllegalArgumentException.class, ClassCastException.class,
			ConversionFailedException.class, NullPointerException.class })
	ResponseEntity<ExceptionMessage> handleMiscFailures(Exception o_O) {

		return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, new HttpHeaders(), o_O);
	}

	/**
	 * Handles {@link RepositoryConstraintViolationException}s by returning {@code 400 Bad Request}.
	 * 
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler
	ResponseEntity<RepositoryConstraintViolationExceptionMessage> handleRepositoryConstraintViolationException(
			RepositoryConstraintViolationException o_O) {
		return response(HttpStatus.BAD_REQUEST, new HttpHeaders(), new RepositoryConstraintViolationExceptionMessage(o_O,
				messageSourceAccessor));
	}

	/**
	 * Send a {@code 409 Conflict} in case of concurrent modification.
	 *
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler({ OptimisticLockingFailureException.class, DataIntegrityViolationException.class })
	ResponseEntity<ExceptionMessage> handleConflict(Exception o_O) {
		return errorResponse(HttpStatus.CONFLICT, new HttpHeaders(), o_O);
	}

	/**
	 * Send {@code 405 Method Not Allowed} and include the supported {@link org.springframework.http.HttpMethod}s in the
	 * {@code Allow} header.
	 *
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler
	ResponseEntity<Void> handle(HttpRequestMethodNotSupportedException o_O) {

		HttpHeaders headers = new HttpHeaders();
		headers.setAllow(o_O.getSupportedHttpMethods());

		return response(HttpStatus.METHOD_NOT_ALLOWED, headers);
	}

	/**
	 * Handles {@link ETagDoesntMatchException} by returning {@code 412 Precondition Failed}.
	 * 
	 * @param o_O the exception to handle.
	 * @return
	 */
	@ExceptionHandler
	ResponseEntity<Void> handle(ETagDoesntMatchException o_O) {

		HttpHeaders headers = o_O.getExpectedETag().addTo(new HttpHeaders());
		return response(HttpStatus.PRECONDITION_FAILED, headers);
	}

	private static ResponseEntity<?> notFound(HttpHeaders headers) {
		return response(HttpStatus.NOT_FOUND, headers, null);
	}

	private static ResponseEntity<ExceptionMessage> badRequest(HttpHeaders headers, Exception throwable) {
		return errorResponse(HttpStatus.BAD_REQUEST, headers, throwable);
	}

	private static ResponseEntity<ExceptionMessage> errorResponse(HttpStatus status, HttpHeaders headers,
			Exception exception) {

		if (exception != null) {

			String message = exception.getMessage();
			LOG.error(message, exception);

			if (StringUtils.hasText(message)) {
				return response(status, headers, new ExceptionMessage(exception));
			}
		}

		return response(status, headers, null);
	}

	private static <T> ResponseEntity<T> response(HttpStatus status, HttpHeaders headers) {
		return response(status, headers, null);
	}

	private static <T> ResponseEntity<T> response(HttpStatus status, HttpHeaders headers, T body) {

		Assert.notNull(headers, "Headers must not be null!");
		Assert.notNull(status, "HttpStatus must not be null!");

		return new ResponseEntity<T>(body, headers, status);
	}
}
