org.opentestsystem.shared.web.AbstractRestController.java Source code

Java tutorial

Introduction

Here is the source code for org.opentestsystem.shared.web.AbstractRestController.java

Source

/*******************************************************************************
 * Educational Online Test Delivery System
 * Copyright (c) 2013 American Institutes for Research
 *
 * Distributed under the AIR Open Source License, Version 1.0
 * See accompanying file AIR-License-1_0.txt or at
 * http://www.smarterapp.org/documents/American_Institutes_for_Research_Open_Source_Software_License.pdf
 ******************************************************************************/
package org.opentestsystem.shared.web;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

import org.apache.commons.lang.StringUtils;
import org.opentestsystem.shared.exception.LocalizedException;
import org.opentestsystem.shared.exception.ResourceNotFoundException;
import org.opentestsystem.shared.exception.SecureAccessRequiredException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.ModelAndView;

import com.fasterxml.jackson.databind.JsonMappingException.Reference;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

@ControllerAdvice
public abstract class AbstractRestController {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRestController.class);
    private static final String EXCEPTION_VIEW_NAME = "exception";
    private static final String BIND_EXCEPTION_MESSAGE = "bind.exception";
    private static final String INVALID_FORMAT_MESSAGE = "invalid.format";

    private static final String MULTIPART_EXCEPTION_MESSAGE = "multipart.exception";
    private static final String DEFAULT_EXCEPTION_MESSAGE = "unexpected.error";
    private static Random random = new Random();
    private static final int MAX_ERROR_CODE = 100000;

    public static final String REFERENCE_NUMBER_KEY = "refNumber";

    @Autowired
    protected MessageSource messageSource;

    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ResponseError handleException(final Exception exception) {
        final String message = exception.getMessage();

        String endUserMessage;
        if (exception instanceof LocalizedException) {
            endUserMessage = getLocalizedMessage((LocalizedException) exception);
        } else if (exception instanceof MultipartException) {
            endUserMessage = getLocalizedMessage(MULTIPART_EXCEPTION_MESSAGE, new String[0]);
        } else {
            endUserMessage = getLocalizedMessage(DEFAULT_EXCEPTION_MESSAGE, new String[0]);
        }

        final String referenceNumber = String.valueOf(random.nextInt(MAX_ERROR_CODE));
        MDC.put(REFERENCE_NUMBER_KEY, referenceNumber);
        LOGGER.error(wrapMessageWithErrorCode(referenceNumber, message), exception);
        MDC.remove(REFERENCE_NUMBER_KEY);
        return new ResponseError(wrapMessageWithErrorCode(referenceNumber, endUserMessage));
    }

    private String getLocalizedMessage(final LocalizedException localizedException) {
        final String messageCode = localizedException.getMessageCode();
        final String[] messageArgs = localizedException.getMessageArgs();
        return getLocalizedMessage(messageCode, messageArgs);
    }

    private String getLocalizedMessage(final String messageCode, final String[] messageArgs) {
        final String localizedMessage = this.messageSource.getMessage(messageCode, messageArgs, messageCode,
                Locale.US);
        if (localizedMessage.equals(messageCode)) {
            LOGGER.debug(localizedMessage + "Unable to find localized message for: " + messageCode);
        }
        return localizedMessage;
    }

    private String wrapMessageWithErrorCode(final String referenceNumber, final String message) {
        return "Error Code: " + referenceNumber + " - " + message;
    }

    // =================================================================================

    private static final Function<Object, String> TO_STRING_FUNCTION = new Function<Object, String>() {
        @Override
        public String apply(final Object obj) {
            return obj == null ? "" : obj.toString();
        }
    };

    /**
     * Catch validation exception and return customized error message
     */
    @SuppressWarnings("rawtypes")
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ResponseError handleConstraintViolationException(final ConstraintViolationException except) {
        final Map<String, List<String>> errorsByField = Maps.newTreeMap();
        for (final ConstraintViolation error : except.getConstraintViolations()) {
            if (errorsByField.get(error.getPropertyPath().toString()) == null) {
                errorsByField.put(error.getPropertyPath().toString(), new ArrayList<String>());
            }
            final List<String> messageList = errorsByField.get(error.getPropertyPath().toString());

            final List<String> args = Lists.newArrayList(error.getPropertyPath().toString(),
                    error.getInvalidValue().toString());
            if (error.getMessage() != null) {
                final Iterable<String> argsToAdd = Iterables.transform(Arrays.asList(error.getMessage().split(",")),
                        TO_STRING_FUNCTION);
                args.addAll(Lists.newArrayList(argsToAdd));
            }
            // This error message code exists for student validation messages on the external API. This fixes a problem where the key was presented instead of the value
            String errorMessage = getLocalizedMessage(error.getMessageTemplate(),
                    args.toArray(new String[args.size()]));
            messageList.add(errorMessage.equals(error.getMessageTemplate()) ? error.getMessage() : errorMessage);
        }

        // sort error messages
        for (final Map.Entry<String, List<String>> entry : errorsByField.entrySet()) {
            Collections.sort(entry.getValue());
        }

        return new ResponseError(errorsByField);
    }

    /**
     * Catch validation exception and return customized error message
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ResponseError handleMethodArgumentNotValidException(final MethodArgumentNotValidException except) {
        final List<FieldError> errors = except.getBindingResult().getFieldErrors();
        final Map<String, List<String>> errorsByField = new TreeMap<String, List<String>>();
        for (final FieldError error : errors) {
            if (errorsByField.get(error.getField()) == null) {
                errorsByField.put(error.getField(), new ArrayList<String>());
            }
            final List<String> messageList = errorsByField.get(error.getField());
            String rejectedValue = "";
            if (error.getRejectedValue() == null) {
                rejectedValue = "null";
            } else {
                rejectedValue = error.getRejectedValue().toString();
            }
            final List<String> args = Lists.newArrayList(error.getField(), rejectedValue);
            if (error.getArguments() != null) {
                final Iterable<String> argsToAdd = Iterables.transform(Arrays.asList(error.getArguments()),
                        TO_STRING_FUNCTION);
                args.addAll(Lists.newArrayList(argsToAdd));
            }
            messageList.add(getLocalizedMessage(error.getDefaultMessage(), args.toArray(new String[args.size()])));
        }

        // sort error messages
        for (final Map.Entry<String, List<String>> entry : errorsByField.entrySet()) {
            Collections.sort(entry.getValue());
        }

        return new ResponseError(errorsByField);
    }

    private static final Function<Reference, String> FIELD_NAME_SELECTOR = new Function<Reference, String>() {
        @Override
        public String apply(final Reference reference) {
            return reference != null ? reference.getFieldName() : "";
        }
    };

    /**
     * Catch validation exception and return customized error message
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ResponseError handleInvalidFormatException(final HttpMessageNotReadableException except)
            throws LocalizedException {
        ResponseError err = null;
        if (except.getCause() instanceof InvalidFormatException) {
            final InvalidFormatException invalidFormatEx = (InvalidFormatException) except.getCause();
            final String[] fieldNames = Iterables
                    .toArray(Iterables.transform(invalidFormatEx.getPath(), FIELD_NAME_SELECTOR), String.class);

            final String path = StringUtils.join(fieldNames, ".");
            String msgArg = getLocalizedMessage(path, null);
            if (path.equals(msgArg)) {
                LOGGER.warn("unable to find " + path);
                msgArg = camelToPretty(fieldNames[fieldNames.length - 1]);
            }
            err = new ResponseError(getLocalizedMessage(INVALID_FORMAT_MESSAGE,
                    new String[] { msgArg, invalidFormatEx.getValue().toString() }));
        } else {
            err = new ResponseError(getLocalizedMessage(BIND_EXCEPTION_MESSAGE, null));
        }
        return err;
    }

    /**
     * Catch validation exception and return customized error message
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    @ResponseBody
    public ResponseError handleAccessDeniedException(final AccessDeniedException except) {
        LOGGER.error("Permissions Issue", except);
        final ResponseError err = new ResponseError(
                "You are not authorized to access this portion of the application, please verify your roles with your Administrator");
        return err;
    }

    /**
     * Prevent user from accessing secured endpoints via HTTP
     */
    @ExceptionHandler(SecureAccessRequiredException.class)
    @ResponseStatus(value = HttpStatus.FORBIDDEN)
    @ResponseBody
    public ResponseError handleSecureAccessRequiredException(final SecureAccessRequiredException except) {
        LOGGER.error("Secure HTTPS required", except);
        final ResponseError err = new ResponseError("This endpoint is only accessible via secure HTTPS");
        return err;
    }

    private static String camelToPretty(final String inputString) {
        final String value = inputString.replaceAll(String.format("%s|%s|%s", "(?<=[A-Z])(?=[A-Z][a-z])",
                "(?<=[^A-Z])(?=[A-Z])", "(?<=[A-Za-z])(?=[^A-Za-z])"), " ");
        return StringUtils.capitalize(value);
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    public ModelAndView handleResourceNotFoundException(final ResourceNotFoundException except) {
        // TODO: Will the path from the calling controller entry point be used to find the view?
        return new ModelAndView(EXCEPTION_VIEW_NAME, "exception", except);
    }
}