//@include lib_Class.js

/**
 * HTTP request utilities
 *
 * @author ELO Digital Office GmbH
 *
 * @eloas
 * @eloix
 * @elojc
 *
 * @requires sol.common.WfUtils
 * @requires sol.common.AsUtils
 */
sol.define("sol.common.HttpUtils", {
  singleton: true,

  /**
   * Sends a HTTP request.
   *
   * Example:
   *
   *     var responseObj = sol.common.HttpUtils.sendRequest({
   *       url: "{{eloWfBaseUrl}}/apps/rest/cmd/app/deploy",
   *       method: "post",
   *       connectTimeout: 10000,
   *       readTimeout: 60000,
   *       contentType: "application/json;charset=UTF-8",
   *       params: { Key2: "Value2" },
   *       addTicket: true,
   *       cookies: { Key1: "Value1" },
   *       addCookieTicket: true
   *     });
   *
   *    var responseObj = sol.common.HttpUtils.sendRequest({
   *       url: "https://example.com"
   *       trustAllHosts: true,
   *       trustAllCerts: true
   *    });
   *
   * URL placeholders:
   *
   *     {{eloWfBaseUrl}} ELOwf base URL
   *     {{eloAsBaseUrl}} ELOas base URL
   *     {{ticket}} ELO authorization ticket
   *
   * Response object:
   *
   *     {
   *       "responseCode": 200,
   *       "responseOk": true,
   *       "content": "",
   *       "errorMessage": ""
   *     }
   *
   * @param {Object} config HTTP request configuration object
   * @param {String} config.url URL
   * @param {String} [config.method=get] Request method, e.g. `get`, `post` or `put`
   * @param {String} config.connectTimeout Connect timeout
   * @param {String} config.readTimeout Read timeout
   * @param {String} config.contentType Content type
   * @param {String} config.user User name
   * @param {String} config.password Password
   * @param {String} [config.authType=Basic] Authentification type
   * @param {Object} config.params Map of key-value pairs which will be submitted as HTTP parameters
   * @param {String} config.addTicket. If true the ELO authorization ticket will be added as HTTP parameter
   * @param {Object} config.cookies Map of key-value pairs which will be added as cookies
   * @param {Object} config.data Data
   * @param {Boolean} [config.encodeData=true] Encode data
   * @param {Object} config.dataObj Data object
   * @param {String} config.addCookieTicket. If true the ELO authorization ticket will be added as cookie
   * @param {Object} config.requestProperties Request properties
   * @param {Boolean} config.resolve Enrich the URL with specific variables (`{{eloWfBaseUrl}}`, `{{ticket}}`) and given parameters
   * @param {String} config.proxyHost Proxy host
   * @param {String} config.proxyPort Proxy port
   * @return {java.net.HttpURLConnection} httpConn. HTTP connection
   * @return {Object} HTTP response object
   */
  sendRequest: function (config) {
    var me = this,
        result;

    config = config || {};
    config.url += "";
    config.resolve = (typeof config.resolve != "undefined") ? config.resolve : !!config.params || (config.url.indexOf("{{") > -1);
    config.encodeData = (typeof config.encodeData == "undefined") ? true : config.encodeData;
    config.method = (config.method || "").toLowerCase();

    if ((config.method == "post") || (config.method == "put")) {
      if (config.dataObj) {
        config.contentType = "application/json;charset=UTF-8";
        config.data = JSON.stringify(config.dataObj);
      }
      result = me.sendPost(config.url, config.data, config);
    } else {
      result = me.sendGet(config.url, config);
    }
    return result;
  },

  /**
   * Sends a HTTP-GET request
   * @private
   * @param {String} urlString URL
   * @param {Object} config HTTP request configuration object
   * @return {Object} Response object
   */
  sendGet: function (urlString, config) {
    var me = this,
        urlConn, resultObj;

    config = config || {};

    if (config.resolve) {
      urlString = me.resolveUrl(urlString, config);
    }
    resultObj = me.buildResultObj(urlString);

    try {
      urlConn = me.prepareRequest(urlString, config);
      urlConn.requestMethod = "GET";
      urlConn.connect();
      me.readResponse(urlConn, resultObj);
    } catch (ex) {
      resultObj.errorMessage = ex;
      me.logger.warn(["Exception while HttpGet: {0}", ex]);
    } finally {
      if (urlConn) {
        urlConn.disconnect();
      }
    }
    return resultObj;
  },

  /**
   * Sends a HTTP-POST request
   * @private
   * @param {String} urlString URL
   * @param {String} data Data
   * @param {Object} config HTTP request configuration object
   * @return {Object} Response object
   */
  sendPost: function (urlString, data, config) {
    var me = this,
        urlConn, outputStream, resultObj, encodeData;

    config = config || {};

    if (config.resolve) {
      urlString = me.resolveUrl(urlString, config);
    }
    resultObj = me.buildResultObj(urlString);

    try {
      urlConn = me.prepareRequest(urlString, config);
      data = data || "";
      if (config.encodeData) {
        encodeData = java.net.URLEncoder.encode(data, "UTF-8");
      } else {
        encodeData = new java.lang.String(data);
      }

      urlConn.doOutput = true;
      urlConn.requestMethod = config.method.toUpperCase();
      urlConn.connect();
      outputStream = urlConn.outputStream;
      outputStream.write(encodeData.getBytes("UTF-8"));
      outputStream.close();
      me.readResponse(urlConn, resultObj);
    } catch (ex) {
      resultObj.errorMessage = ex;
      me.logger.warn(["Exception while HttpPost: {0}", ex]);
    } finally {
      if (urlConn) {
        urlConn.disconnect();
      }
    }
    return resultObj;
  },

  /**
   * @private
   * Prepares a HTTP request
   * @param {String} urlString URL
   * @param {Object} config HTTP request configuration object
   * @return {java.net.HttpURLConnection}
   */
  prepareRequest: function (urlString, config) {
    var me = this,
        cookiesArr = [],
        url, urlObj, proxy, urlConn, userpass, basicAuth, key, trustManager, sslContext, value,
        connectTimeout, readTimeout;

    url = urlString;
    urlObj = new java.net.URL(url);

    if (config.proxyHost) {
      proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, new java.net.InetSocketAddress(config.proxyHost, config.proxyPort));
      urlConn = urlObj.openConnection(proxy);
    } else {
      urlConn = urlObj.openConnection();
    }

    connectTimeout = java.lang.Integer.valueOf(config.connectTimeout || 10000);
    readTimeout = java.lang.Integer.valueOf(config.readTimeout || 60000);

    urlConn.setConnectTimeout(connectTimeout);
    urlConn.setReadTimeout(readTimeout);

    if (config.contentType) {
      urlConn.setRequestProperty("Content-Type", config.contentType);
    }
    if (config.user) {
      if (config.authType == "NTLM") {
        java.net.Authenticator.default = new JavaAdapter(java.net.Authenticator, {
          getPasswordAuthentication: function () {
            return new java.net.PasswordAuthentication(config.user, java.lang.String(config.password).toCharArray());
          }
        });
      } else {
        userpass = java.lang.String(config.user + ":" + config.password);
        basicAuth = "Basic " + me.encodeBase64(userpass);
        urlConn.setRequestProperty("Authorization", basicAuth);
      }
    }

    if (config.requestProperties) {
      for (key in config.requestProperties) {
        value = config.requestProperties[key];
        urlConn.setRequestProperty(key, value);
      }
    }

    if (config.addCookieTicket) {
      config.cookies = config.cookies || {};
      config.cookies.ticket = ixConnect.loginResult.clientInfo.ticket;
    }

    if (config.cookies) {
      for (key in config.cookies) {
        cookiesArr.push(key + "=" + config.cookies[key]);
      }
      urlConn.setRequestProperty("Cookie", cookiesArr.join("; "));
    }

    // Avoid SSL exception:
    //   javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching <hostname> found
    if (urlConn.defaultHostnameVerifier && config.trustAllHosts) {
      urlConn.hostnameVerifier = new javax.net.ssl.HostnameVerifier({
        verify: function (hostname, session) {
          return true;
        }
      });
    }

    // Avoid SSL exception:
    //   javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed:
    //   sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    if (urlConn.defaultSSLSocketFactory && config.trustAllCerts) {
      trustManager = new javax.net.ssl.X509TrustManager({
        checkClientTrusted: function (chain, authType) {
        },

        checkServerTrusted: function (chain, authType) {
        },

        getAcceptedIssuers: function () {
          return null;
        }
      });

      sslContext = javax.net.ssl.SSLContext.getInstance("SSL");
      sslContext.init(null, [trustManager], new java.security.SecureRandom());
      urlConn.SSLSocketFactory = sslContext.socketFactory;
    }

    me.logRequestProperties(urlConn);

    return urlConn;
  },

  /**
   * @private
   * @param {java.net.HttpURLConnection} urlConn
   */
  logRequestProperties: function (urlConn) {
    var me = this,
        requestProperties, entrySetIterator, entrySet, requestPropertyName, requestPropertyValueList, requestPropertyValues,
        headerValueListIterator, requestPropertyValue;

    if (me.logger.debugEnabled) {
      me.logger.debug("Request properties:");
      requestProperties = urlConn.requestProperties;
      entrySetIterator = requestProperties.entrySet().iterator();
      while (entrySetIterator.hasNext()) {
        entrySet = entrySetIterator.next();
        requestPropertyName = entrySet.key;
        requestPropertyValueList = entrySet.value;
        requestPropertyValues = [];
        headerValueListIterator = requestPropertyValueList.iterator();
        while (headerValueListIterator.hasNext()) {
          requestPropertyValue = headerValueListIterator.next() + "";
          requestPropertyValues.push(requestPropertyValue);
        }
        me.logger.debug(requestPropertyName + ": " + requestPropertyValues.join(";"));
      }
    }
  },

  /**
   * @private
   * @return {javax.mail.PasswordAuthentication}
   */
  getPasswordAuthentication: function () {
    var me = this,
        authenticator;

    authenticator = new javax.mail.PasswordAuthentication(me.user, me.password);
    return authenticator;
  },

  /**
   * Resolves an URL
   *
   * @param {String} urlString URL
   * @param {Object} config HTTP request configuration object
   * @return {String} URL
   */
  resolveUrl: function (urlString, config) {
    var ticket, wfBaseUrl, asBaseUrl, key,
        paramArr = [];

    config = config || {};

    ticket = ixConnect.loginResult.clientInfo.ticket;
    urlString += "";

    if (urlString.indexOf("{{eloWfBaseUrl}}") > -1) {
      wfBaseUrl = sol.common.WfUtils.getWfBaseUrl();
      urlString = urlString.replace("{{eloWfBaseUrl}}", wfBaseUrl);
    }
    if (urlString.indexOf("{{eloAsBaseUrl}}") > -1) {
      asBaseUrl = sol.common.AsUtils.getAsBaseUrl();
      urlString = urlString.replace("{{eloAsBaseUrl}}", asBaseUrl);
    }

    if (config) {
      if (config.params) {
        for (key in config.params) {
          paramArr.push(encodeURI(key) + "=" + encodeURI(config.params[key]));
        }
      }
      if (config.addTicket) {
        paramArr.push("ticket=" + ticket);
      }
    }

    if (paramArr.length > 0) {
      urlString += "?" + paramArr.join("&");
    }
    urlString = urlString.replace("{{ticket}}", ticket);
    return urlString;
  },

  /**
   * @private
   * Builds a response object
   * @param {String} url URL
   * @return {Object} Response object
   */
  buildResultObj: function (url) {
    var resultObj, logUrl;

    url += "";
    logUrl = url.replace(/(ticket=\w{7})\w+/, "$1...");
    resultObj = { url: logUrl, responseOk: false };
    return resultObj;
  },

  /**
   * @private
   * Read HTTP response
   * @param {java.net.HttpURLConnection} urlConn HTTP connection
   * @param {Object} resultObj Result object
   */
  readResponse: function (urlConn, resultObj) {
    var me = this;

    resultObj.responseCode = urlConn.responseCode;
    if ((urlConn.responseCode >= java.net.HttpURLConnection.HTTP_OK) && (urlConn.responseCode <= java.net.HttpURLConnection.HTTP_PARTIAL)) {
      resultObj.responseOk = true;
      resultObj.content = me.inputStreamToString(urlConn.inputStream);
    } else {
      resultObj.responseOk = false;
      resultObj.content = me.inputStreamToString(urlConn.errorStream);
      resultObj.errorMessage = "HTTP status code " + resultObj.responseCode;
    }
  },

  /**
   * @private
   * Converts the content of an input stream into a string
   * @param {java.io.InputStream} inputStream Input Stream
   * @return {String} Content as string
   */
  inputStreamToString: function (inputStream) {
    if (!inputStream) {
      return "";
    }
    var content = String(Packages.org.apache.commons.io.IOUtils.toString(inputStream, "UTF-8"));
    inputStream.close();
    return content;
  },

  /**
   * Returns a Base64 encoded string
   * @param {String} str String
   * @return {String} Base64 encoded string
   */
  encodeBase64: function (str) {
    var me = this,
        srcBytes, base64Bytes, base64String;
    if (!str) {
      throw "String is empty";
    }
    srcBytes = me.convertStringToByteArray(str);
    base64Bytes = Packages.org.apache.commons.codec.binary.Base64.encodeBase64(srcBytes);
    base64String = me.convertByteArrayToString(base64Bytes);
    return base64String;
  },

  /**
   * @private
   * Converts a string to a byte array
   * @param {String} str String
   * @return {Byte[]}
   */
  convertStringToByteArray: function (str) {
    var bytes;
    if (!str) {
      throw "String is empty";
    }
    bytes = java.lang.String(str).getBytes("UTF-8");
    return bytes;
  },

  /**
   * @private
   * Converts a byte array to a string
   * @param {Byte[]} bytes
   * @return {String}
   */
  convertByteArrayToString: function (bytes) {
    var javaString, str;
    if (!bytes) {
      throw "Bytes are empty";
    }
    javaString = new java.lang.String(bytes, "UTF-8");
    str = String(javaString);
    return str;
  }
});