Java tutorial
/******************************************************************************* * 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); } }