package com.tyndalehouse.step.rest.framework;
import com.tyndalehouse.step.core.exceptions.LocalisedException;
import com.tyndalehouse.step.core.exceptions.StepInternalException;
import com.tyndalehouse.step.core.exceptions.TranslatedException;
import com.tyndalehouse.step.core.exceptions.ValidationException;
import com.tyndalehouse.step.core.models.ClientSession;
import com.tyndalehouse.step.core.service.AppManagerService;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Provider;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;
import java.util.ResourceBundle;
import static java.lang.String.format;
/**
* @author chrisburrell
*/
@MultipartConfig
public abstract class AbstractAjaxController extends HttpServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAjaxController.class);
private final ObjectMapper jsonMapper;
private final transient ClientErrorResolver errorResolver;
private final AppManagerService appManagerService;
private final Provider<ClientSession> clientSessionProvider;
public AbstractAjaxController(final AppManagerService appManagerService,
final Provider<ClientSession> clientSessionProvider,
final ClientErrorResolver errorResolver,
final Provider<ObjectMapper> objectMapperProvider) {
this.appManagerService = appManagerService;
this.clientSessionProvider = clientSessionProvider;
this.errorResolver = errorResolver;
this.jsonMapper = objectMapperProvider.get();
}
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response) {
// CHECKSTYLE:ON
try {
Object returnVal = executeRestMethod(request);
byte[] jsonEncoded = getEncodedJsonResponse(returnVal);
setupHeaders(response, jsonEncoded.length);
response.getOutputStream().write(jsonEncoded);
// CHECKSTYLE:OFF We allow catching errors here, since we are at the top of the structure
} catch (final Exception e) {
// CHECKSTYLE:ON
handleError(response, e, request);
}
}
@Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response) {
this.doGet(request, response);
}
private Object executeRestMethod(final HttpServletRequest request) {
Object returnVal;
try {
returnVal = invokeMethod(request);
// CHECKSTYLE:OFF
} catch (final Exception e) {
LOGGER.warn(e.getMessage());
LOGGER.trace(e.getMessage(), e);
returnVal = convertExceptionToJson(e);
}
return returnVal;
}
protected abstract Object invokeMethod(HttpServletRequest request) throws Exception;
/**
* We attempt here to rethrow the exception that caused the invocation target exception, so that we can handle it
* nicely for the user
*
* @param e the wrapped exception that happened during the reflective call
* @return a client handled issue which wraps the exception that was raised
*/
protected ClientHandledIssue convertExceptionToJson(final Exception e) {
// first we check to see if it's a step exception, or an illegal argument exception
final Throwable cause = e.getCause() == null ? e : e.getCause();
return new ClientHandledIssue(getExceptionMessageAndLog(cause), this.errorResolver.resolve(cause
.getClass()));
}
/**
* Returns a json response that is encoded
*
* @param responseValue the value that should be encoded
* @return the encoded form of the JSON response
*/
byte[] getEncodedJsonResponse(final Object responseValue) {
LOGGER.debug("Encoding the following response [{}]", responseValue);
try {
String response;
if (responseValue == null) {
return new byte[0];
} else {
response = this.jsonMapper.writeValueAsString(responseValue);
}
return response.getBytes(FrontController.UTF_8_ENCODING);
} catch (final JsonGenerationException e) {
throw new StepInternalException(e.getMessage(), e);
} catch (final JsonMappingException e) {
throw new StepInternalException(e.getMessage(), e);
} catch (final IOException e) {
throw new StepInternalException(e.getMessage(), e);
}
}
/**
* sets up the headers and the length of the message
*
* @param response the response
* @param length the length of the message
*/
void setupHeaders(final HttpServletResponse response, final int length) {
// we ensure that headers are set up appropriately
response.addDateHeader("Date", System.currentTimeMillis());
response.setCharacterEncoding(FrontController.UTF_8_ENCODING);
response.setContentType("application/json");
response.setContentLength(length);
response.setHeader("step-language", this.clientSessionProvider.get().getLocale().getLanguage());
response.setHeader("step-version", this.appManagerService.getAppVersion());
}
/**
* deals with an error whilst executing the request
*
* @param response the response
* @param e the exception
*/
void handleError(final HttpServletResponse response, final Throwable e, final HttpServletRequest request) {
LOGGER.debug("Handling error...");
try {
if (e != null) {
final ClientHandledIssue issue = new ClientHandledIssue(getExceptionMessageAndLog(e));
final byte[] errorMessage = this.getEncodedJsonResponse(issue);
response.getOutputStream().write(errorMessage);
setupHeaders(response, errorMessage.length);
}
// CHECKSTYLE:OFF We allow catching errors here, since we are at the top of the structure
} catch (final Exception unableToSendError) {
// CHECKSTYLE:ON
LOGGER.error("Unable to output error for request" + request.getRequestURI(), unableToSendError);
LOGGER.error("Due to original Throwable", e);
}
}
/**
* Gets the exception message.
*
* @param e the e
* @return the exception message
*/
private String getExceptionMessageAndLog(final Throwable e) {
LOGGER.trace("Tracing exception: ", e);
final Locale locale = this.clientSessionProvider.get().getLocale();
final ResourceBundle bundle = ResourceBundle.getBundle("ErrorBundle", locale);
if (!(e instanceof StepInternalException)) {
return returnInternalError(e, bundle);
}
// else we're looking at a STEP caught exception
if (e instanceof LocalisedException) {
return e.getMessage();
}
if (e instanceof TranslatedException) {
final TranslatedException translatedException = (TranslatedException) e;
LOGGER.warn(e.getMessage());
LOGGER.debug(e.getMessage(), e);
return format(bundle.getString(translatedException.getMessage()), translatedException.getArgs());
}
if (e instanceof ValidationException) {
final ValidationException validationException = (ValidationException) e;
switch (validationException.getExceptionType()) {
case LOGIN_REQUIRED:
return bundle.getString("error_login");
case USER_MISSING_FIELD:
return bundle.getString("error_missing_field");
case USER_VALIDATION_ERROR:
return bundle.getString("error_validation");
case APP_MISSING_FIELD:
case CONTROLLER_INITIALISATION_ERROR:
case SERVICE_VALIDATION_ERROR:
default:
return returnInternalError(e, bundle);
}
}
return returnInternalError(e, bundle);
}
/**
* Return internal error.
*
* @param e the e
* @param bundle the bundle
* @return the string
*/
private String returnInternalError(final Throwable e, final ResourceBundle bundle) {
if (e == null) {
LOGGER.error("An unknown internal error has occurred");
} else {
LOGGER.error(e.getMessage(), e);
}
return bundle.getString("error_internal");
}
}