/**
 * 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 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.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.HttpURLConnection;
import java.util.Collections;
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 MethodHandles.Lookup PUBLIC_LOOKUP = MethodHandles.publicLookup();
  private static final MethodType EXCEPTION_CONSTRUCTOR_TYPE =
          MethodType.methodType(void.class, String.class);

  /**
   * 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 =
        Collections.singletonMap(ERROR_JSON, json);
    Writer writer = response.getWriter();
    JsonSerialization.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 = Collections.singletonMap(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 = JsonSerialization.mapReader().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);
            Preconditions.checkState(Exception.class.isAssignableFrom(klass),
                "Class [%s] is not a subclass of Exception", klass);
            MethodHandle methodHandle = PUBLIC_LOOKUP.findConstructor(
                    klass, EXCEPTION_CONSTRUCTOR_TYPE);
            toThrow = (Exception) methodHandle.invoke(exMsg);
          } catch (Throwable t) {
            toThrow = new IOException(String.format(
                "HTTP status [%d], exception [%s], message [%s], URL [%s]",
                conn.getResponseCode(), exClass, exMsg, conn.getURL()));
          }
        } else {
          String msg = (exMsg != null) ? exMsg : conn.getResponseMessage();
          toThrow = new IOException(String.format(
              "HTTP status [%d], message [%s], URL [%s]",
              conn.getResponseCode(), msg, conn.getURL()));
        }
      } catch (Exception ex) {
        toThrow = new IOException(String.format(
            "HTTP status [%d], message [%s], URL [%s], exception [%s]",
            conn.getResponseCode(), conn.getResponseMessage(), conn.getURL(),
            ex.toString()), ex);
      } finally {
        if (es != null) {
          try {
            es.close();
          } catch (IOException ex) {
            //ignore
          }
        }
      }
      throwEx(toThrow);
    }
  }

}
