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