/* $Id: InterruptibleSocketFactory.java 1634296 2014-10-26 10:39:30Z kwright $ */

/**
* 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.manifoldcf.connectorcommon.common;

import java.io.*;
import java.net.*;

import org.apache.http.conn.ConnectTimeoutException;

/** SSL Socket factory which wraps another socket factory but allows timeout on socket
* creation.
*/
public class InterruptibleSocketFactory extends javax.net.ssl.SSLSocketFactory
{
  protected final javax.net.ssl.SSLSocketFactory wrappedFactory;
  protected final long connectTimeoutMilliseconds;
    
  public InterruptibleSocketFactory(javax.net.ssl.SSLSocketFactory wrappedFactory, long connectTimeoutMilliseconds)
  {
    this.wrappedFactory = wrappedFactory;
    this.connectTimeoutMilliseconds = connectTimeoutMilliseconds;
  }

  @Override
  public Socket createSocket()
    throws IOException
  {
    // Socket isn't open
    return wrappedFactory.createSocket();
  }
    
  @Override
  public Socket createSocket(String host, int port)
    throws IOException, UnknownHostException
  {
    return fireOffThread(InetAddress.getByName(host),port,null,-1);
  }

  @Override
  public Socket createSocket(InetAddress host, int port)
    throws IOException
  {
    return fireOffThread(host,port,null,-1);
  }
    
  @Override
  public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
    throws IOException, UnknownHostException
  {
    return fireOffThread(InetAddress.getByName(host),port,localHost,localPort);
  }
    
  @Override
  public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
    throws IOException
  {
    return fireOffThread(address,port,localAddress,localPort);
  }
    
  @Override
  public Socket createSocket(Socket s, String host, int port, boolean autoClose)
    throws IOException
  {
    // Socket's already open
    return wrappedFactory.createSocket(s,host,port,autoClose);
  }
    
  @Override
  public String[] getDefaultCipherSuites()
  {
    return wrappedFactory.getDefaultCipherSuites();
  }
    
  @Override
  public String[] getSupportedCipherSuites()
  {
    return wrappedFactory.getSupportedCipherSuites();
  }
    
  protected Socket fireOffThread(InetAddress address, int port, InetAddress localHost, int localPort)
    throws IOException
  {
    SocketCreateThread thread = new SocketCreateThread(wrappedFactory,address,port,localHost,localPort);
    thread.start();
    try
    {
      // Wait for thread to complete for only a certain amount of time!
      thread.join(connectTimeoutMilliseconds);
      // If join() times out, then the thread is going to still be alive.
      if (thread.isAlive())
      {
        // Kill the thread - not that this will necessarily work, but we need to try
        thread.interrupt();
        throw new ConnectTimeoutException("Secure connection timed out");
      }
      // The thread terminated.  Throw an error if there is one, otherwise return the result.
      Throwable t = thread.getException();
      if (t != null)
      {
        if (t instanceof java.net.SocketTimeoutException)
          throw (java.net.SocketTimeoutException)t;
        else if (t instanceof ConnectTimeoutException)
          throw (ConnectTimeoutException)t;
        else if (t instanceof InterruptedIOException)
          throw (InterruptedIOException)t;
        else if (t instanceof IOException)
          throw (IOException)t;
        else if (t instanceof Error)
          throw (Error)t;
        else if (t instanceof RuntimeException)
          throw (RuntimeException)t;
        throw new Error("Received an unexpected exception: "+t.getMessage(),t);
      }
      return thread.getResult();
    }
    catch (InterruptedException e)
    {
      throw new InterruptedIOException("Interrupted: "+e.getMessage());
    }

  }

  /** Create a secure socket in a thread, so that we can "give up" after a while if the socket fails to connect.
  */
  protected static class SocketCreateThread extends Thread
  {
    // Socket factory
    protected javax.net.ssl.SSLSocketFactory socketFactory;
    protected InetAddress host;
    protected int port;
    protected InetAddress clientHost;
    protected int clientPort;

    // The return socket
    protected Socket rval = null;
    // The return error
    protected Throwable throwable = null;

    /** Create the thread */
    public SocketCreateThread(javax.net.ssl.SSLSocketFactory socketFactory,
      InetAddress host,
      int port,
      InetAddress clientHost,
      int clientPort)
    {
      this.socketFactory = socketFactory;
      this.host = host;
      this.port = port;
      this.clientHost = clientHost;
      this.clientPort = clientPort;
      setDaemon(true);
    }

    public void run()
    {
      try
      {
        if (clientHost == null)
          rval = socketFactory.createSocket(host,port);
        else
          rval = socketFactory.createSocket(host,port,clientHost,clientPort);
      }
      catch (Throwable e)
      {
        throwable = e;
      }
    }

    public Throwable getException()
    {
      return throwable;
    }

    public Socket getResult()
    {
      return rval;
    }
  }

}
