| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.hadoop.util; |
| |
| import org.apache.hadoop.classification.InterfaceAudience; |
| import org.apache.hadoop.classification.InterfaceStability; |
| import org.codehaus.jackson.map.ObjectMapper; |
| import org.codehaus.jackson.map.ObjectReader; |
| import org.codehaus.jackson.map.ObjectWriter; |
| |
| import javax.servlet.http.HttpServletResponse; |
| import javax.ws.rs.core.MediaType; |
| import javax.ws.rs.core.Response; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.Writer; |
| import java.lang.reflect.Constructor; |
| import java.net.HttpURLConnection; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| |
| /** |
| * HTTP utility class to help propagate server side exception to the client |
| * over HTTP as a JSON payload. |
| * <p/> |
| * It creates HTTP Servlet and JAX-RPC error responses including details of the |
| * exception that allows a client to recreate the remote exception. |
| * <p/> |
| * It parses HTTP client connections and recreates the exception. |
| */ |
| @InterfaceAudience.Private |
| @InterfaceStability.Unstable |
| public class HttpExceptionUtils { |
| |
| public static final String ERROR_JSON = "RemoteException"; |
| public static final String ERROR_EXCEPTION_JSON = "exception"; |
| public static final String ERROR_CLASSNAME_JSON = "javaClassName"; |
| public static final String ERROR_MESSAGE_JSON = "message"; |
| |
| private static final String APPLICATION_JSON_MIME = "application/json"; |
| |
| private static final String ENTER = System.getProperty("line.separator"); |
| |
| private static final ObjectReader READER = |
| new ObjectMapper().reader(Map.class); |
| private static final ObjectWriter WRITER = |
| new ObjectMapper().writerWithDefaultPrettyPrinter(); |
| |
| /** |
| * Creates a HTTP servlet response serializing the exception in it as JSON. |
| * |
| * @param response the servlet response |
| * @param status the error code to set in the response |
| * @param ex the exception to serialize in the response |
| * @throws IOException thrown if there was an error while creating the |
| * response |
| */ |
| public static void createServletExceptionResponse( |
| HttpServletResponse response, int status, Throwable ex) |
| throws IOException { |
| response.setStatus(status); |
| response.setContentType(APPLICATION_JSON_MIME); |
| Map<String, Object> json = new LinkedHashMap<String, Object>(); |
| json.put(ERROR_MESSAGE_JSON, getOneLineMessage(ex)); |
| json.put(ERROR_EXCEPTION_JSON, ex.getClass().getSimpleName()); |
| json.put(ERROR_CLASSNAME_JSON, ex.getClass().getName()); |
| Map<String, Object> jsonResponse = new LinkedHashMap<String, Object>(); |
| jsonResponse.put(ERROR_JSON, json); |
| Writer writer = response.getWriter(); |
| WRITER.writeValue(writer, jsonResponse); |
| writer.flush(); |
| } |
| |
| /** |
| * Creates a HTTP JAX-RPC response serializing the exception in it as JSON. |
| * |
| * @param status the error code to set in the response |
| * @param ex the exception to serialize in the response |
| * @return the JAX-RPC response with the set error and JSON encoded exception |
| */ |
| public static Response createJerseyExceptionResponse(Response.Status status, |
| Throwable ex) { |
| Map<String, Object> json = new LinkedHashMap<String, Object>(); |
| json.put(ERROR_MESSAGE_JSON, getOneLineMessage(ex)); |
| json.put(ERROR_EXCEPTION_JSON, ex.getClass().getSimpleName()); |
| json.put(ERROR_CLASSNAME_JSON, ex.getClass().getName()); |
| Map<String, Object> response = new LinkedHashMap<String, Object>(); |
| response.put(ERROR_JSON, json); |
| return Response.status(status).type(MediaType.APPLICATION_JSON). |
| entity(response).build(); |
| } |
| |
| private static String getOneLineMessage(Throwable exception) { |
| String message = exception.getMessage(); |
| if (message != null) { |
| int i = message.indexOf(ENTER); |
| if (i > -1) { |
| message = message.substring(0, i); |
| } |
| } |
| return message; |
| } |
| |
| // trick, riding on generics to throw an undeclared exception |
| |
| private static void throwEx(Throwable ex) { |
| HttpExceptionUtils.<RuntimeException>throwException(ex); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static <E extends Throwable> void throwException(Throwable ex) |
| throws E { |
| throw (E) ex; |
| } |
| |
| /** |
| * Validates the status of an <code>HttpURLConnection</code> against an |
| * expected HTTP status code. If the current status code is not the expected |
| * one it throws an exception with a detail message using Server side error |
| * messages if available. |
| * <p/> |
| * <b>NOTE:</b> this method will throw the deserialized exception even if not |
| * declared in the <code>throws</code> of the method signature. |
| * |
| * @param conn the <code>HttpURLConnection</code>. |
| * @param expectedStatus the expected HTTP status code. |
| * @throws IOException thrown if the current status code does not match the |
| * expected one. |
| */ |
| @SuppressWarnings("unchecked") |
| public static void validateResponse(HttpURLConnection conn, |
| int expectedStatus) throws IOException { |
| if (conn.getResponseCode() != expectedStatus) { |
| Exception toThrow; |
| InputStream es = null; |
| try { |
| es = conn.getErrorStream(); |
| Map json = READER.readValue(es); |
| json = (Map) json.get(ERROR_JSON); |
| String exClass = (String) json.get(ERROR_CLASSNAME_JSON); |
| String exMsg = (String) json.get(ERROR_MESSAGE_JSON); |
| if (exClass != null) { |
| try { |
| ClassLoader cl = HttpExceptionUtils.class.getClassLoader(); |
| Class klass = cl.loadClass(exClass); |
| Constructor constr = klass.getConstructor(String.class); |
| toThrow = (Exception) constr.newInstance(exMsg); |
| } catch (Exception ex) { |
| toThrow = new IOException(String.format( |
| "HTTP status [%d], exception [%s], message [%s] ", |
| conn.getResponseCode(), exClass, exMsg)); |
| } |
| } else { |
| String msg = (exMsg != null) ? exMsg : conn.getResponseMessage(); |
| toThrow = new IOException(String.format( |
| "HTTP status [%d], message [%s]", conn.getResponseCode(), msg)); |
| } |
| } catch (Exception ex) { |
| toThrow = new IOException(String.format( |
| "HTTP status [%d], message [%s]", conn.getResponseCode(), |
| conn.getResponseMessage())); |
| } finally { |
| if (es != null) { |
| try { |
| es.close(); |
| } catch (IOException ex) { |
| //ignore |
| } |
| } |
| } |
| throwEx(toThrow); |
| } |
| } |
| |
| } |