001package systems.dmx.core.util; 002 003import systems.dmx.core.JSONEnabled; 004import systems.dmx.core.service.accesscontrol.AccessControlException; 005 006import org.codehaus.jettison.json.JSONException; 007import org.codehaus.jettison.json.JSONObject; 008 009import javax.servlet.http.HttpServletRequest; 010import javax.servlet.http.HttpServletResponse; 011 012import javax.ws.rs.WebApplicationException; 013import javax.ws.rs.core.MediaType; 014import javax.ws.rs.core.MultivaluedMap; 015import javax.ws.rs.core.Response; 016import javax.ws.rs.core.Response.ResponseBuilder; 017import javax.ws.rs.core.Response.Status; 018import javax.ws.rs.core.Response.Status.Family; 019import javax.ws.rs.core.Response.StatusType; 020 021import java.io.IOException; 022import java.util.logging.Level; 023import java.util.logging.Logger; 024 025 026 027/** 028 * Maps exceptions to suitable HTTP responses. 029 * <p> 030 * Supports both, JAX-RS responses and Servlet API responses. 031 * <p> 032 * 2 additional aspects are handled: 033 * - Logging the exception. 034 * - Enriching the response with an error entity. 035 */ 036public class UniversalExceptionMapper { 037 038 // ------------------------------------------------------------------------------------------------------- Constants 039 040 // Note: status 405 is not defined by JAX-RS 041 private static StatusType METHOD_NOT_ALLOWED = new StatusType() { 042 @Override public int getStatusCode() {return 405;} 043 @Override public String getReasonPhrase() {return "Method Not Allowed";} 044 @Override public Family getFamily() {return Family.CLIENT_ERROR;} 045 }; 046 047 // ---------------------------------------------------------------------------------------------- Instance Variables 048 049 private Throwable e; 050 private HttpServletRequest request; 051 052 private Logger logger = Logger.getLogger(getClass().getName()); 053 054 // ---------------------------------------------------------------------------------------------------- Constructors 055 056 public UniversalExceptionMapper(Throwable e, HttpServletRequest request) { 057 this.e = e; 058 this.request = request; 059 } 060 061 // -------------------------------------------------------------------------------------------------- Public Methods 062 063 public Response toResponse() { 064 Response response; 065 if (e instanceof WebApplicationException) { 066 response = ((WebApplicationException) e).getResponse(); 067 StatusType status = fromStatusCode(response.getStatus()); 068 Family family = status.getFamily(); 069 // Don't log redirects like 304 Not Modified 070 if (family == Family.CLIENT_ERROR || family == Family.SERVER_ERROR) { 071 Throwable cause = e.getCause(); 072 Throwable originalException = cause != null ? cause : e; 073 logException(status, originalException); 074 // Only set entity if not already provided by application 075 if (response.getEntity() == null) { 076 response = errorResponse(Response.fromResponse(response), originalException); 077 } 078 } 079 } else { 080 // build generic response 081 StatusType status = hasNestedAccessControlException(e) ? Status.UNAUTHORIZED : Status.INTERNAL_SERVER_ERROR; 082 logException(status, e); 083 response = errorResponse(Response.status(status), e); 084 } 085 return response; 086 } 087 088 public void initResponse(HttpServletResponse response) throws IOException { 089 transferResponse(toResponse(), response); 090 } 091 092 // ------------------------------------------------------------------------------------------------- Private Methods 093 094 private void logException(StatusType status, Throwable e) { 095 logger.log(Level.SEVERE, "Request \"" + JavaUtils.requestInfo(request) + "\" failed. Responding with " + 096 JavaUtils.responseInfo(status) + ". The original exception/error is:", e); 097 } 098 099 private Response errorResponse(ResponseBuilder builder, Throwable e) { 100 return builder.type(MediaType.APPLICATION_JSON).entity(new ExceptionInfo(e)).build(); 101 } 102 103 private boolean hasNestedAccessControlException(Throwable e) { 104 while (e != null) { 105 if (e instanceof AccessControlException) { 106 return true; 107 } 108 e = e.getCause(); 109 } 110 return false; 111 } 112 113 private StatusType fromStatusCode(int statusCode) { 114 StatusType status; 115 if (statusCode == 405) { 116 status = METHOD_NOT_ALLOWED; 117 } else { 118 status = Status.fromStatusCode(statusCode); 119 if (status == null) { 120 throw new RuntimeException(statusCode + " is an unexpected status code"); 121 } 122 } 123 return status; 124 } 125 126 // --- 127 128 /** 129 * Transfers status code, headers, and entity of a JAX-RS Response to a HttpServletResponse and sends the response. 130 */ 131 private void transferResponse(Response response, HttpServletResponse servletResponse) throws IOException { 132 // status code 133 servletResponse.setStatus(response.getStatus()); 134 // headers 135 MultivaluedMap<String, Object> metadata = response.getMetadata(); 136 for (String header : metadata.keySet()) { 137 for (Object value : metadata.get(header)) { 138 servletResponse.addHeader(header, value.toString()); 139 } 140 } 141 // entity 142 servletResponse.getWriter().write(response.getEntity().toString()); // throws IOException 143 // servletResponse.sendError(response.getStatus(), (String) response.getEntity()); // throws IOException 144 } 145 146 // --------------------------------------------------------------------------------------------------- Private Class 147 148 private static class ExceptionInfo implements JSONEnabled { 149 150 private JSONObject json; 151 152 private ExceptionInfo(Throwable e) { 153 try { 154 json = createJSONObject(e); 155 } catch (JSONException je) { 156 throw new RuntimeException("Generating exception info failed", je); 157 } 158 } 159 160 private JSONObject createJSONObject(Throwable e) throws JSONException { 161 String message = e.getMessage(); // may be null 162 JSONObject json = new JSONObject() 163 .put("exception", e.getClass().getName()) 164 .put("message", message != null ? message : ""); 165 // 166 Throwable cause = e.getCause(); 167 if (cause != null) { 168 json.put("cause", createJSONObject(cause)); 169 } 170 // 171 return json; 172 } 173 174 @Override 175 public JSONObject toJSON() { 176 return json; 177 } 178 179 @Override 180 public String toString() { 181 return json.toString(); 182 } 183 } 184}