/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2003-2006 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License 1.0 which accompanies this     **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl - initial API and implementation (bpasero@rssowl.org)         **
 **                                                                          **
 **  **********************************************************************  */

package net.sourceforge.rssowl.dao;

import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.controller.dialog.LoginDialog;
import net.sourceforge.rssowl.controller.thread.ExtendedThread;
import net.sourceforge.rssowl.dao.ssl.EasySSLProtocolSocketFactory;
import net.sourceforge.rssowl.model.Category;
import net.sourceforge.rssowl.model.Favorite;
import net.sourceforge.rssowl.util.CryptoManager;
import net.sourceforge.rssowl.util.GlobalSettings;
import net.sourceforge.rssowl.util.shop.BrowserShop;
import net.sourceforge.rssowl.util.shop.ProxyShop;
import net.sourceforge.rssowl.util.shop.StringShop;
import net.sourceforge.rssowl.util.shop.URLShop;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.eclipse.jface.window.Window;

import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;

/**
 * The ConnectionManager establishes a connection to the given URL and handles
 * Proxy and Authentification. As result, the InputStream is returned. Since the
 * ConnectionManager will be instantiated out of parallel running Threads, it is
 * important to check if the current executing Thread was not yet stopped.
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class ConnectionManager {

  /** Http Error Code: Authentication Required */
  private static final int HTTP_AUTH_REQUIRED = 401;

  /** Begin of all Http Error Codes */
  private static final int HTTP_ERRORS = 400;

  private HttpClient client;
  private InputStream inputStream;
  private boolean showLoginDialogIfRequired;
  String domain;
  GetMethod getMethod;
  String password;
  String url;
  String userAgent;
  String username;

  /**
   * Instantiate a new ConnectionManager to establish a connection to the given
   * URL
   * 
   * @param url The URL to connect to
   */
  public ConnectionManager(String url) {
    this.url = URLShop.canonicalize(url);
    username = null;
    password = null;
    showLoginDialogIfRequired = true;
    userAgent = BrowserShop.getOwlAgent();

    /** Check if user, passwd and domain are stored in the CryptoManager */
    if (CryptoManager.getInstance().hasItem(url)) {
      String credentials[] = CryptoManager.getInstance().getItem(url).split(StringShop.AUTH_TOKENIZER);

      /** Basic or Digest Credentials without Domain */
      if (credentials.length == 2) {
        username = credentials[0];
        password = credentials[1];
      }

      /** NTLM Credentials with Domain */
      else if (credentials.length == 3) {
        username = credentials[0];
        password = credentials[1];
        domain = credentials[2];
      }
    }
  }

  /**
   * This will allow to use any SSL certificate without validation. Allows to
   * use the HTTPS protocol with the HttpClient.
   */
  public static void initSSLProtocol() {

    /** Register Easy Protocol Socket Factory with HTTPS */
    Protocol easyHttpsProtocol = new Protocol("https", (ProtocolSocketFactory) new EasySSLProtocolSocketFactory(), 443);
    Protocol.registerProtocol("https", easyHttpsProtocol);
  }

  /**
   * Closes the connection
   */
  public void closeConnection() {
    if (getMethod != null)
      getMethod.releaseConnection();
  }

  /**
   * Connect to the given URL and handle Proxy and Authentification if required
   * 
   * @throws IOException In case an error occurs
   * @throws HttpException In case an error occurs
   */
  public void connect() throws HttpException, IOException {

    /** Check for proxy usage */
    boolean useProxy = ProxyShop.isUseProxy();

    /** Try to get custom settings from favorite if available */
    if (Category.getFavPool().containsKey(url)) {
      Favorite rssOwlFavorite = (Favorite) Category.getFavPool().get(url);

      /** Apply custom proxy settings */
      useProxy = (ProxyShop.isUseProxy() && rssOwlFavorite.isUseProxy());
    }

    /** Connect */
    connect(useProxy);
  }

  /**
   * Connect to the given URL and handle Proxy and Authentification if required
   * 
   * @param useProxy Force usage of Proxy
   * @throws IOException In case an error occurs
   * @throws HttpException In case an error occurs
   */
  public void connect(boolean useProxy) throws HttpException, IOException {

    /** Init the connection */
    initConnection();

    /** Update proxy in HttpClient if required */
    if (useProxy && proceed())
      ProxyShop.setUpProxy(client);

    /** Open the connection and handle authentification if required */
    openConnection();

    /** Try to pipe the resulting stream into a GZipInputStream */
    pipeConnection();

    /** If status code is 4xx, throw an IOException with the status code included */
    if (proceed() && getMethod.getStatusCode() >= HTTP_ERRORS)
      throw new IOException(String.valueOf(getMethod.getStatusCode()));

    /**
     * It is possible that GetMethod#getResponseBodyAsStream() returns NULL if a
     * response body is not available. Throw an undefined IOException in that
     * case.
     */
    if (proceed() && inputStream == null)
      throw new IOException();
  }

  /**
   * Get the resulting InputStream
   * 
   * @return InputStream The resulting InputStream from the connection
   */
  public InputStream getInputStream() {
    return inputStream;
  }

  /**
   * Get the statusline that the server has sent as a response to the connection
   * 
   * @return String The responded statusline to the connect
   */
  public String getStatusLine() {
    if (getMethod != null && getMethod.getStatusLine() != null)
      return getMethod.getStatusLine().toString();
    return null;
  }

  /**
   * Set wether to show the LoginDialog in case authentification is required
   * 
   * @param showLoginDialogIfRequired If TRUE, show the LoginDialog if required
   */
  public void setShowLoginDialogIfRequired(boolean showLoginDialogIfRequired) {
    this.showLoginDialogIfRequired = showLoginDialogIfRequired;
  }

  /**
   * Set the user agent to use in the response header
   * 
   * @param userAgent The user agent to use
   */
  public void setUserAgent(String userAgent) {
    this.userAgent = userAgent;
  }

  /**
   * The connection requires authentification. Open the LoginDialog in case
   * username and password are not stored in the CryptoManager. Use the
   * credentials to authenticate to the Server. Supported authentification
   * methods are Base, Digest and NTLM
   * 
   * @throws URIException In case the provided URL was not correct
   */
  private void authenticate() throws URIException {

    /** Ask Username and Password from the User */
    if (username == null && showLoginDialogIfRequired && proceed()) {
      GUI.display.syncExec(new Runnable() {
        public void run() {

          /** New baseauth dialog - Check for NTLM Authentication */
          boolean isNtlm = false;
          if (getMethod != null && getMethod.getHostAuthState().getAuthScheme() != null)
            isNtlm = "ntlm".equals(getMethod.getHostAuthState().getAuthScheme().getSchemeName());

          LoginDialog loginDialog = new LoginDialog(GUI.shell, GUI.i18n.getTranslation("BASE_AUTH_TITLE"), GUI.i18n.getTranslation("BASE_AUTH_MESSAGE"), url, isNtlm);
          int status = loginDialog.open();

          /** User has pressed OK */
          if (status == Window.OK) {
            username = loginDialog.getUsername();
            password = loginDialog.getPassword();
            domain = loginDialog.getDomain();
          }
        }
      });
    }

    /** Username must not be null */
    if (username == null)
      return;

    /** Require Host */
    String host = getMethod.getURI().getHost();

    /** Create the UsernamePasswordCredentials */
    NTCredentials userPwCreds = new NTCredentials(username, password, host, (domain != null) ? domain : "");

    /** Authenticate to the Server */
    client.getState().setCredentials(AuthScope.ANY, userPwCreds);
    getMethod.setDoAuthentication(true);
  }

  /**
   * Initialize the connection with creating a HttpClient object and a GET
   * method object with the given URL
   * 
   * @throws IOException In case of an error while creating the GET-Method.
   */
  private void initConnection() throws IOException {
    client = new HttpClient();

    /** Socket Timeout - Max. time to wait for an answer */
    client.getHttpConnectionManager().getParams().setSoTimeout(GlobalSettings.connectionTimeout * 1000);

    /** Connection Timeout - Max. time to wait for a connection */
    client.getHttpConnectionManager().getParams().setConnectionTimeout(GlobalSettings.connectionTimeout * 1000);

    /** Create the Get Method. Wrap any RuntimeException into an IOException */
    try {
      getMethod = new GetMethod(url);
    } catch (RuntimeException e) {
      throw new IOException(e.getMessage());
    }

    /** Ignore Cookies */
    getMethod.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);

    /** Set Headers */
    setHeaders();

    /** Follow Redirects */
    getMethod.setFollowRedirects(true);
  }

  /**
   * Open the connection and retrieve the InputStream from the Server.
   * 
   * @throws IOException If an error occurs
   * @throws HttpException If an error occurs
   */
  private void openConnection() throws HttpException, IOException {

    /** The username might have been stored in the CryptoManager already */
    if (username != null) {
      client.getParams().setAuthenticationPreemptive(true);
      authenticate();
    }

    /** Execute the GET Method */
    if (proceed())
      client.executeMethod(getMethod);

    /** In case Preemptive Authentication failed, show login dialog */
    if (username != null && getMethod.getStatusCode() == HTTP_AUTH_REQUIRED) {
      client.getParams().setAuthenticationPreemptive(false);
      username = null;
    }

    /**
     * In case the connection requires authentification and username was not yet
     * passed through due to a preemptive authentication or an authentication
     * failed, send username and password and execute the GET method again.
     */
    if (!client.getParams().isAuthenticationPreemptive() && getMethod.getHostAuthState().isAuthRequested() && proceed()) {
      authenticate();
      client.executeMethod(getMethod);
    }

    /** Finally retrieve the InputStream from the respond body */
    if (proceed())
      inputStream = getMethod.getResponseBodyAsStream();
  }

  /**
   * Try to pipe the resulting InputStream into a GZipInputStream
   * 
   * @throws IOException If an error occurs
   */
  private void pipeConnection() throws IOException {

    /** Retrieve the Content Encoding */
    String contentEncoding = getMethod.getResponseHeader("Content-Encoding") != null ? getMethod.getResponseHeader("Content-Encoding").getValue() : null;
    boolean isGzipStream = false;

    /**
     * Return in case the Content Encoding is not given and the InputStream does
     * not support mark() and reset()
     */
    if ((contentEncoding == null || !contentEncoding.equals("gzip")) && (inputStream == null || !inputStream.markSupported()))
      return;

    /** Content Encoding is set to gzip, so use the GZipInputStream */
    if (contentEncoding != null && contentEncoding.equals("gzip")) {
      isGzipStream = true;
    }

    /** Detect if the Stream is gzip encoded */
    else if (inputStream != null && inputStream.markSupported()) {
      inputStream.mark(2);
      int id1 = inputStream.read();
      int id2 = inputStream.read();
      inputStream.reset();

      /** Check for GZip Magic Numbers (See RFC 1952) */
      if (id1 == 0x1F && id2 == 0x8B)
        isGzipStream = true;
    }

    /** Create the GZipInputStream then */
    if (isGzipStream && proceed())
      inputStream = new GZIPInputStream(inputStream);
  }

  /**
   * Checks wether the current running Thread was not yet stopped.
   * 
   * @return boolean TRUE in case the execution may proceed
   */
  private boolean proceed() {
    if (Thread.currentThread() instanceof ExtendedThread)
      return !((ExtendedThread) Thread.currentThread()).isStopped();
    return !Thread.currentThread().isInterrupted();
  }

  /**
   * Set default headers to the connection
   */
  private void setHeaders() {
    getMethod.setRequestHeader("Accept-Encoding", "gzip, *");
    getMethod.setRequestHeader("User-Agent", userAgent);
  }

  /**
   * Init the SSL Support for HTTPS connections through HttpClient.
   */
  static {
    initSSLProtocol();
  }
}