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}