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}