/**
* Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.util.rest;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletContext;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.sun.jersey.api.client.UniformInterfaceException;
/**
* An abstract class to assist with writing JAX-RS exception mappers.
*
* @param <T> the mapped exception type
*/
public abstract class AbstractExceptionMapper<T extends Throwable>
implements ExceptionMapper<T> {
/** Logger. */
protected static final Logger s_logger = LoggerFactory.getLogger(AbstractExceptionMapper.class);
/**
* The RESTful request headers.
*/
@Context
private HttpHeaders _headers;
/**
* The servlet context.
*/
@Context
private ServletContext _servletContext;
/**
* Creates the mapper.
*/
protected AbstractExceptionMapper() {
}
//-------------------------------------------------------------------------
@Override
public Response toResponse(T exception) {
return createResponse(exception);
}
/**
* Creates the JAX-RS response for the exception.
* <p>
* This is the main method invoked by subclasses.
*
* @param exception the exception being processed, not null
* @return the response, not null
*/
public Response createResponse(T exception) {
if (_headers.getAcceptableMediaTypes().contains(MediaType.TEXT_HTML_TYPE)) {
String page = buildHtmlErrorPage(exception);
logHtmlException(exception, page);
return doHtmlResponse(exception, page);
} else {
logRestfulError(exception);
return doRestfulResponse(exception);
}
}
//-------------------------------------------------------------------------
/**
* Provides the HTML error page.
*
* @param exception the exception being processed, not null
* @return the HTML error page, null if none
*/
protected String buildHtmlErrorPage(T exception) {
return null;
}
/**
* Creates the HTML error page.
*
* @param errorResource the resource, not null
* @param data the substitution data, not null
* @return the page, null if no page
*/
protected String createHtmlErrorPage(String errorResource, Map<String, String> data) {
try (InputStream in = _servletContext.getResourceAsStream("/WEB-INF/pages/errors/" + errorResource)) {
if (in == null) {
s_logger.debug("AbstractExceptionMapper resource not found: /WEB-INF/pages/errors/" + errorResource);
return null;
}
List<String> lines = IOUtils.readLines(in, StandardCharsets.UTF_8);
for (ListIterator<String> it = lines.listIterator(); it.hasNext(); ) {
String line = (String) it.next();
for (Entry<String, String> entry : data.entrySet()) {
line = StringUtils.replace(line, "${" + entry.getKey() + "}", entry.getValue());
it.set(line);
}
}
return Joiner.on('\n').join(lines);
} catch (IOException | RuntimeException ex) {
s_logger.debug("AbstractExceptionMapper error", ex);
return null;
}
}
/**
* Builds the output message for the exception into the data map.
*
* @param exception the exception being processed, may be null
* @param data the substitution data, not null
*/
protected void buildOutputMessage(Throwable exception, Map<String, String> data) {
// includes HTML tags in locator so exception could be switched off in future
if (exception == null) {
data.put("message", "");
} else if (exception.getMessage() == null) {
if (exception.getCause() != null) {
buildOutputMessage(exception.getCause(), data);
} else {
data.put("message", "");
data.put("locator", "<p>" + errorLocator(exception) + "</p>");
}
} else {
Throwable rootCause = Throwables.getRootCause(exception);
if (rootCause instanceof UniformInterfaceException) {
rootCause = exception;
}
String message = exception.getMessage();
String rootMessage = rootCause.getMessage();
if (message.contains(rootMessage) == false) {
message = message + " caused by " + rootMessage;
}
data.put("message", message);
if (Throwables.getRootCause(exception) instanceof UniformInterfaceException == false) {
data.put("locator", "<p>" + errorLocator(rootCause) + "</p>");
} else {
data.put("locator", "");
}
}
}
private String errorLocator(Throwable exception) {
String base = exception.getClass().getSimpleName();
if (exception.getStackTrace().length == 0) {
return base;
}
StrBuilder buf = new StrBuilder(512);
buf.append(base).append("<br />");
int count = 0;
for (int i = 0; i < exception.getStackTrace().length && count < 4; i++) {
StackTraceElement ste = exception.getStackTrace()[i];
if (ste.getClassName().startsWith("sun.") || ste.getClassName().startsWith("javax.") || ste.getClassName().startsWith("com.sun.") ||
(ste.getClassName().equals("java.lang.reflect.Method") && ste.getMethodName().equals("invoke"))) {
continue;
}
if (ste.getLineNumber() >= 0) {
buf.append(String.format(" at %s.%s() L%d<br />", ste.getClassName(), ste.getMethodName(), ste.getLineNumber()));
} else {
buf.append(String.format(" at %s.%s()<br />", ste.getClassName(), ste.getMethodName()));
}
count++;
}
return buf.toString();
}
/**
* Returns the HTML response.
*
* @param exception the exception being processed, not null
* @param htmlPage the HTML page, may be null
* @return the response, not null
*/
protected abstract Response doHtmlResponse(T exception, String htmlPage);
/**
* Logs the error in the HTML scenario.
*
* @param exception the exception, not null
* @param htmlPage the HTML page, may be null
*/
protected void logHtmlException(T exception, String htmlPage) {
if (htmlPage != null) {
s_logger.debug("RESTful website exception caught: " + packStackTrace(exception));
} else {
s_logger.info("RESTful website exception caught", exception);
}
}
//-------------------------------------------------------------------------
/**
* Returns the RESTful response.
*
* @param exception the exception being processed, not null
* @return the response, not null
*/
protected abstract Response doRestfulResponse(T exception);
/**
* Logs the error in the RESTful scenario.
*
* @param exception the exception, not null
*/
protected void logRestfulError(final T exception) {
s_logger.info("RESTful web-service exception caught and tunnelled to client:", exception);
}
//-------------------------------------------------------------------------
/**
* Converts an exception to a short stack trace.
*
* @param exception the exception, not null
* @return the short stack trace, not null
*/
protected String packStackTrace(T exception) {
StackTraceElement[] stackTrace = exception.getStackTrace();
switch (stackTrace.length) {
case 0:
return "Unknown";
case 1:
return stackTrace[0].toString();
case 2:
return stackTrace[0].toString() + " \n" + stackTrace[1].toString();
default:
return stackTrace[0].toString() + " \n" + stackTrace[1].toString() + " \n" + stackTrace[2].toString();
}
}
}