Client.java [src/csip/utils] Revision:   Date:
/*
 * $Id$
 *
 * This file is part of the Cloud Services Integration Platform (CSIP),
 * a Model-as-a-Service framework, API and application suite.
 *
 * 2012-2022, Olaf David and others, OMSLab, Colorado State University.
 *
 * OMSLab licenses this file to you under the MIT license.
 * See the LICENSE file in the project root for more information.
 */
package csip.utils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_OK;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.codehaus.jettison.json.JSONObject;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.codehaus.jettison.json.JSONException;

import org.apache.http.client.HttpResponseException;

/**
 * HTTP Client for easier CSIP service calling.
 *
 * @author od
 */
public class Client implements AutoCloseable {

  private Logger log;

  private CloseableHttpClient httpClient;

  public static final int DEF_TIMEOUT = 3600; // 1 h timeout

  private static final PoolingHttpClientConnectionManager ncm = getCM();


  private static PoolingHttpClientConnectionManager getCM() {
    PoolingHttpClientConnectionManager cm
        = new PoolingHttpClientConnectionManager(
            RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", new SSLConnectionSocketFactory(SSLContexts.createSystemDefault()))
                .build()
        );
    cm.setMaxTotal(250);
    cm.setDefaultMaxPerRoute(100);
    cm.setMaxPerRoute(new HttpRoute(new HttpHost("locahost", 80)), 150);
    return cm;
  }


  /**
   * Create a Client
   */
  public Client() {
    this(DEF_TIMEOUT, null);
  }


  /**
   * Create a client with a logger.
   *
   * @param log the Logger
   *
   */
  public Client(Logger log) {
    this(DEF_TIMEOUT, log);
  }


  /**
   * Create a client with a custom timeout.
   *
   * @param timeout the timeout in
   */
  public Client(int timeout) {
    this(timeout, null);
  }


  public Client(int timeout, Logger log) {
    this.log = log;
    RequestConfig c = RequestConfig.custom()
        .setConnectTimeout(timeout * 1000)
        .setConnectionRequestTimeout(timeout * 1000)
        .setSocketTimeout(timeout * 1000)
        .build();
    httpClient = HttpClients.custom()
        .setDefaultRequestConfig(c)
        .setConnectionManagerShared(true)
        .setConnectionManager(ncm)
        .build();
  }


  @Override
  public void close() {
    try {
      httpClient.close();
    } catch (IOException ex) {
      if (log != null)
        log.log(Level.SEVERE, null, ex);
    }
  }


  /**
   * Pings a HTTP URL. This effectively sends a HEAD request and returns
   * <code>true</code> if the response code is in the 200-399 range.
   *
   * @param url The HTTP URL to be pinged.
   * @param timeout The timeout in millis for both the connection timeout and
   * the response read timeout. Note that the total timeout is effectively two
   * times the given timeout.
   * @return the time to respond if the given HTTP URL has returned response
   * code 200-399 on a HEAD request within the given timeout, otherwise
   * <code>-1</code>.
   */
  public static long ping(String url, int timeout) {
//    url = url.replaceFirst("^https", "http");
    HttpURLConnection conn = null;
    try {
      conn = (HttpURLConnection) new URL(url).openConnection();
      conn.setConnectTimeout(timeout);
      conn.setReadTimeout(timeout);
      conn.setRequestMethod("HEAD");
      long start = System.currentTimeMillis();
      int responseCode = conn.getResponseCode();
      long end = System.currentTimeMillis();
      return (200 <= responseCode && responseCode <= 399) ? (end - start) : -1;
    } catch (Exception E) {
      return -1;
    }
  }


  public static boolean ping0(String url, int timeout) {
    try {
      HttpURLConnection.setFollowRedirects(true);
      HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
      con.setConnectTimeout(timeout);
      con.setRequestMethod("HEAD");
      return (con.getResponseCode() == HttpURLConnection.HTTP_OK);
    } catch (Exception E) {
      return false;
    }
  }

////////////////// GET   

  public File doGET(String url, File file) throws Exception {
    return (File) _doGET(url, fw(t -> {
      FileUtils.copyInputStreamToFile(t, file);
      return file;
    }));
  }


  public String doGET(String url) throws Exception {
    return (String) _doGET(url, fw(t -> {
      return IOUtils.toString(t, "UTF-8");
    }));
  }


  private Object _doGET(String url, Function<InputStream, Object> c) throws Exception {
    if (log != null && log.isLoggable(Level.INFO))
      log.info("GET : " + url);

    HttpGet get = new HttpGet(url);
    try (CloseableHttpResponse httpResponse = httpClient.execute(get)) {
      int statusCode = httpResponse.getStatusLine().getStatusCode();

      if (statusCode < HTTP_BAD_REQUEST)  // < 400
        return c.apply(httpResponse.getEntity().getContent());
      String reason = httpResponse.getStatusLine().getReasonPhrase();
      throw new IOException(url + ": error " + statusCode, new HttpResponseException(statusCode, reason));
    } finally {
      get.releaseConnection();
    }
  }

  @FunctionalInterface
  interface ThrowingFunction<T, R, E extends Exception> {

    R apply(T t) throws E;
  }


  static <T, R> Function<T, R> fw(ThrowingFunction<T, R, Exception> throwingFunction) {
    return i -> {
      try {
        return throwingFunction.apply(i);
      } catch (Exception ex) {
        throw new RuntimeException(ex);
      }
    };
  }

///////////// DELETE  

  public int doDelete(String url) throws Exception {
    HttpDelete del = new HttpDelete(url);
    int status = 0;
    try {
      HttpResponse resultsResponse = httpClient.execute(del);
      status = resultsResponse.getStatusLine().getStatusCode();
    } finally {
      del.releaseConnection();
    }
    if (log != null && log.isLoggable(Level.INFO)) {
      log.info("DELETE : " + url + " -> " + status);
    }
    return status;
  }

///////////// POST   

  /**
   * HTTP Post with the request JSON and files attached.
   *
   * @param uri the endpoint
   * @param req the request JSON
   * @param files the files to attach
   * @param header the header map
   * @return the response JSON
   */
  public JSONObject doPOST(String uri, JSONObject req, File[] files, Map<String, String> header) {
    return doPOST(uri, req, files, null, header);
  }


  /**
   * HTTP Post with the request JSON.
   *
   * @param uri the endpoint
   * @param req the request JSON
   * @return the response JSON
   * @throws Exception if something goes wrong
   */
  public JSONObject doPOST(String uri, JSONObject req) throws Exception {
    return doPOST(uri, req, null, null, null);
  }


  public JSONObject doPOST(String uri, JSONObject req, Map<String, String> header) throws Exception {
    return doPOST(uri, req, null, null, header);
  }


  public String doPOST(String uri, String req) throws Exception {
    return doPOST(uri, req, null, null, null);
  }


  public JSONObject doPOST(String uri, JSONObject req, File[] files, String[] names) throws Exception {
    return doPOST(uri, req, files, names, null);
  }


  public JSONObject doPOST(String uri, JSONObject req, File[] files) throws Exception {
    return doPOST(uri, req, files, null, null);
  }


  public JSONObject doPOST(String uri, JSONObject req, File[] files, String[] names, Map<String, String> header) {
    try {
      String r = doPOST(uri, req.toString(), files, names, header);
      return new JSONObject(r);
    } catch (JSONException ex) {
      return new JSONObject(new HashMap<String, String>() {
        {
          put("error", ex.getMessage());
        }
      });
    }
  }


  public String doPOST(String uri, String req, File[] files, String[] names, Map<String, String> header) {
    if (uri == null || uri.isEmpty())
      throw new IllegalArgumentException("URI is null or empty.");

    if (req == null || req.isEmpty())
      throw new IllegalArgumentException("Invalid request, null or empty.");

    String response;
    HttpPost post = new HttpPost(uri);

    if (files != null && files.length > 0) {
      MultipartEntityBuilder builder = MultipartEntityBuilder.create();
      builder.addPart("param", new StringBody(req, ContentType.APPLICATION_JSON));
      if (log != null && log.isLoggable(Level.INFO))
        log.info(" attaching request as 'param':" + req);

      for (int i = 0; i < files.length; i++) {
        FileBody b = new FileBody(files[i], ContentType.APPLICATION_OCTET_STREAM,
            names == null ? files[i].getName() : names[i]);
        builder.addPart("file" + (i + 1), b);
        if (log != null && log.isLoggable(Level.INFO))
          log.info(" attached file" + b.getFile().toString() + " as " + b.getFilename());

      }
      post.setEntity(builder.build());
    } else {
      post.addHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
      StringEntity entity = new StringEntity(req, "UTF-8");
      entity.setContentType(ContentType.APPLICATION_JSON.toString());
      if (log != null && log.isLoggable(Level.INFO))
        log.info("string entity : " + req);

      post.setEntity(entity);
    }

    if (header != null) {
      for (Map.Entry<String, String> entry : header.entrySet()) {
        post.addHeader(entry.getKey(), entry.getValue());
        if (log != null && log.isLoggable(Level.INFO))
          log.info("adding header: " + entry.getKey() + ": " + entry.getValue());
      }
    }

    if (log != null && log.isLoggable(Level.INFO))
      log.info("POST: " + uri + " ...");

    try (CloseableHttpResponse httpResponse = httpClient.execute(post)) {
      int statusCode = httpResponse.getStatusLine().getStatusCode();
      if (statusCode == HTTP_OK) {
        InputStream responseStream = httpResponse.getEntity().getContent();
        response = IOUtils.toString(responseStream, "UTF-8");
        if (log != null && log.isLoggable(Level.INFO)) {
          log.info("response: " + response);
          PoolStats ps = ncm.getTotalStats();
          log.info("http-client-connections pool [avail=" + ps.getAvailable()
              + " leased=" + ps.getLeased() + " pending=" + ps.getPending() + "] ");
        }
      } else {
        if (log != null)
          log.severe("response code: " + statusCode);

        response = "{'error': " + statusCode + "}";
      }
    } catch (Exception E) {
      if (log != null)
        log.log(Level.SEVERE, "Client Error: ", E);

      response = "{'error': '" + E.getMessage() + "'}";
    } finally {
      post.releaseConnection();
    }
    return response;
  }

  //////////////////// PUT

  public int doPUT(String uri, JSONObject req) throws Exception {
    HttpPut put = new HttpPut(uri);
    put.addHeader("Content-Type", "application/json");
    StringEntity entity = new StringEntity(req.toString(), "UTF-8");
    entity.setContentType("application/json");
    if (log != null && log.isLoggable(Level.INFO))
      log.info("attaching : " + req.toString());

    put.setEntity(entity);
    if (log != null && log.isLoggable(Level.INFO))
      log.info("PUT : " + uri);

    int statusCode = 0;
    try (CloseableHttpResponse response = httpClient.execute(put)) {
      statusCode = response.getStatusLine().getStatusCode();
    } finally {
      put.releaseConnection();
    }
    return statusCode;
  }
}