/*
 * 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.flume.api;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.avro.ipc.CallFuture;
import org.apache.avro.ipc.NettyTransceiver;
import org.apache.avro.ipc.Transceiver;
import org.apache.avro.ipc.specific.SpecificRequestor;
import org.apache.commons.lang.StringUtils;
import org.apache.flume.Event;
import org.apache.flume.EventDeliveryException;
import org.apache.flume.FlumeException;
import org.apache.flume.source.avro.AvroFlumeEvent;
import org.apache.flume.source.avro.AvroSourceProtocol;
import org.apache.flume.source.avro.Status;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.socket.SocketChannel;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.compression.ZlibDecoder;
import org.jboss.netty.handler.codec.compression.ZlibEncoder;
import org.jboss.netty.handler.ssl.SslHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Avro/Netty implementation of {@link RpcClient}.
 * The connections are intended to be opened before clients are given access so
 * that the object cannot ever be in an inconsistent when exposed to users.
 */
public class NettyAvroRpcClient extends AbstractRpcClient implements RpcClient {

  private ExecutorService callTimeoutPool;
  private final ReentrantLock stateLock = new ReentrantLock();

  /**
   * Guarded by {@code stateLock}
   */
  private ConnState connState;

  private InetSocketAddress address;
  private boolean enableSsl;
  private boolean trustAllCerts;
  private String truststore;
  private String truststorePassword;
  private String truststoreType;
  private final List<String> excludeProtocols = new LinkedList<String>();

  private Transceiver transceiver;
  private AvroSourceProtocol.Callback avroClient;
  private static final Logger logger = LoggerFactory
      .getLogger(NettyAvroRpcClient.class);
  private boolean enableDeflateCompression;
  private int compressionLevel;
  private int maxIoWorkers;

  /**
   * This constructor is intended to be called from {@link RpcClientFactory}.
   * A call to this constructor should be followed by call to configure().
   */
  protected NettyAvroRpcClient(){
  }

  /**
   * This method should only be invoked by the build function
   * @throws FlumeException
   */
  private void connect() throws FlumeException {
    connect(connectTimeout, TimeUnit.MILLISECONDS);
  }

  /**
   * Internal only, for now
   * @param timeout
   * @param tu
   * @throws FlumeException
   */
  private void connect(long timeout, TimeUnit tu) throws FlumeException {
    callTimeoutPool = Executors.newCachedThreadPool(
        new TransceiverThreadFactory("Flume Avro RPC Client Call Invoker"));
    NioClientSocketChannelFactory socketChannelFactory = null;

    try {

      ExecutorService bossExecutor =
          Executors.newCachedThreadPool(new TransceiverThreadFactory(
              "Avro " + NettyTransceiver.class.getSimpleName() + " Boss"));
      ExecutorService workerExecutor =
          Executors.newCachedThreadPool(new TransceiverThreadFactory(
              "Avro " + NettyTransceiver.class.getSimpleName() + " I/O Worker"));

      if (enableDeflateCompression || enableSsl) {
        if (maxIoWorkers >= 1) {
          socketChannelFactory = new SSLCompressionChannelFactory(
            bossExecutor, workerExecutor,
            enableDeflateCompression, enableSsl, trustAllCerts,
            compressionLevel, truststore, truststorePassword, truststoreType,
            excludeProtocols, maxIoWorkers);
        } else {
          socketChannelFactory = new SSLCompressionChannelFactory(
            bossExecutor, workerExecutor,
            enableDeflateCompression, enableSsl, trustAllCerts,
            compressionLevel, truststore, truststorePassword, truststoreType,
            excludeProtocols);
        }
      } else {
        if (maxIoWorkers >= 1) {
          socketChannelFactory = new NioClientSocketChannelFactory(
              bossExecutor, workerExecutor, maxIoWorkers);
        } else {
          socketChannelFactory = new NioClientSocketChannelFactory(
              bossExecutor, workerExecutor);
        }
      }

      transceiver = new NettyTransceiver(this.address,
          socketChannelFactory,
          tu.toMillis(timeout));
      avroClient =
          SpecificRequestor.getClient(AvroSourceProtocol.Callback.class,
          transceiver);
    } catch (Throwable t) {
      if (callTimeoutPool != null) {
        callTimeoutPool.shutdownNow();
      }
      if (socketChannelFactory != null) {
        socketChannelFactory.releaseExternalResources();
      }
      if (t instanceof IOException) {
        throw new FlumeException(this + ": RPC connection error", t);
      } else if (t instanceof FlumeException) {
        throw (FlumeException) t;
      } else if (t instanceof Error) {
        throw (Error) t;
      } else {
        throw new FlumeException(this + ": Unexpected exception", t);
      }
    }

    setState(ConnState.READY);
  }

  @Override
  public void close() throws FlumeException {
    if (callTimeoutPool != null) {
      callTimeoutPool.shutdown();
      try {
        if (!callTimeoutPool.awaitTermination(requestTimeout,
            TimeUnit.MILLISECONDS)) {
          callTimeoutPool.shutdownNow();
          if (!callTimeoutPool.awaitTermination(requestTimeout,
              TimeUnit.MILLISECONDS)) {
            logger.warn(this + ": Unable to cleanly shut down call timeout " +
                "pool");
          }
        }
      } catch (InterruptedException ex) {
        logger.warn(this + ": Interrupted during close", ex);
        // re-cancel if current thread also interrupted
        callTimeoutPool.shutdownNow();
        // preserve interrupt status
        Thread.currentThread().interrupt();
      }

      callTimeoutPool = null;
    }
    try {
      transceiver.close();
    } catch (IOException ex) {
      throw new FlumeException(this + ": Error closing transceiver.", ex);
    } finally {
      setState(ConnState.DEAD);
    }

  }

  @Override
  public String toString() {
    return "NettyAvroRpcClient { host: " + address.getHostName() + ", port: " +
        address.getPort() + " }";
  }

  @Override
  public void append(Event event) throws EventDeliveryException {
    try {
      append(event, requestTimeout, TimeUnit.MILLISECONDS);
    } catch (Throwable t) {
      // we mark as no longer active without trying to clean up resources
      // client is required to call close() to clean up resources
      setState(ConnState.DEAD);
      if (t instanceof Error) {
        throw (Error) t;
      }
      if (t instanceof TimeoutException) {
        throw new EventDeliveryException(this + ": Failed to send event. " +
            "RPC request timed out after " + requestTimeout + "ms", t);
      }
      throw new EventDeliveryException(this + ": Failed to send event", t);
    }
  }

  private void append(Event event, long timeout, TimeUnit tu)
      throws EventDeliveryException {

    assertReady();

    final CallFuture<Status> callFuture = new CallFuture<Status>();

    final AvroFlumeEvent avroEvent = new AvroFlumeEvent();
    avroEvent.setBody(ByteBuffer.wrap(event.getBody()));
    avroEvent.setHeaders(toCharSeqMap(event.getHeaders()));

    Future<Void> handshake;
    try {
      // due to AVRO-1122, avroClient.append() may block
      handshake = callTimeoutPool.submit(new Callable<Void>() {

        @Override
        public Void call() throws Exception {
          avroClient.append(avroEvent, callFuture);
          return null;
        }
      });
    } catch (RejectedExecutionException ex) {
      throw new EventDeliveryException(this + ": Executor error", ex);
    }

    try {
      handshake.get(connectTimeout, TimeUnit.MILLISECONDS);
    } catch (TimeoutException ex) {
      throw new EventDeliveryException(this + ": Handshake timed out after " +
          connectTimeout + " ms", ex);
    } catch (InterruptedException ex) {
      throw new EventDeliveryException(this + ": Interrupted in handshake", ex);
    } catch (ExecutionException ex) {
      throw new EventDeliveryException(this + ": RPC request exception", ex);
    } catch (CancellationException ex) {
      throw new EventDeliveryException(this + ": RPC request cancelled", ex);
    } finally {
      if (!handshake.isDone()) {
        handshake.cancel(true);
      }
    }

    waitForStatusOK(callFuture, timeout, tu);
  }

  @Override
  public void appendBatch(List<Event> events) throws EventDeliveryException {
    try {
      appendBatch(events, requestTimeout, TimeUnit.MILLISECONDS);
    } catch (Throwable t) {
      // we mark as no longer active without trying to clean up resources
      // client is required to call close() to clean up resources
      setState(ConnState.DEAD);
      if (t instanceof Error) {
        throw (Error) t;
      }
      if (t instanceof TimeoutException) {
        throw new EventDeliveryException(this + ": Failed to send event. " +
            "RPC request timed out after " + requestTimeout + " ms", t);
      }
      throw new EventDeliveryException(this + ": Failed to send batch", t);
    }
  }

  private void appendBatch(List<Event> events, long timeout, TimeUnit tu)
      throws EventDeliveryException {

    assertReady();

    Iterator<Event> iter = events.iterator();
    final List<AvroFlumeEvent> avroEvents = new LinkedList<AvroFlumeEvent>();

    // send multiple batches... bail if there is a problem at any time
    while (iter.hasNext()) {
      avroEvents.clear();

      for (int i = 0; i < batchSize && iter.hasNext(); i++) {
        Event event = iter.next();
        AvroFlumeEvent avroEvent = new AvroFlumeEvent();
        avroEvent.setBody(ByteBuffer.wrap(event.getBody()));
        avroEvent.setHeaders(toCharSeqMap(event.getHeaders()));
        avroEvents.add(avroEvent);
      }

      final CallFuture<Status> callFuture = new CallFuture<Status>();

      Future<Void> handshake;
      try {
        // due to AVRO-1122, avroClient.appendBatch() may block
        handshake = callTimeoutPool.submit(new Callable<Void>() {

          @Override
          public Void call() throws Exception {
            avroClient.appendBatch(avroEvents, callFuture);
            return null;
          }
        });
      } catch (RejectedExecutionException ex) {
        throw new EventDeliveryException(this + ": Executor error", ex);
      }

      try {
        handshake.get(connectTimeout, TimeUnit.MILLISECONDS);
      } catch (TimeoutException ex) {
        throw new EventDeliveryException(this + ": Handshake timed out after " +
            connectTimeout + "ms", ex);
      } catch (InterruptedException ex) {
        throw new EventDeliveryException(this + ": Interrupted in handshake",
            ex);
      } catch (ExecutionException ex) {
        throw new EventDeliveryException(this + ": RPC request exception", ex);
      } catch (CancellationException ex) {
        throw new EventDeliveryException(this + ": RPC request cancelled", ex);
      } finally {
        if (!handshake.isDone()) {
          handshake.cancel(true);
        }
      }

      waitForStatusOK(callFuture, timeout, tu);
    }
  }

  /**
   * Helper method that waits for a Status future to come back and validates
   * that it returns Status == OK.
   * @param callFuture Future to wait on
   * @param timeout Time to wait before failing
   * @param tu Time Unit of {@code timeout}
   * @throws EventDeliveryException If there is a timeout or if Status != OK
   */
  private void waitForStatusOK(CallFuture<Status> callFuture,
      long timeout, TimeUnit tu) throws EventDeliveryException {
    try {
      Status status = callFuture.get(timeout, tu);
      if (status != Status.OK) {
        throw new EventDeliveryException(this + ": Avro RPC call returned " +
            "Status: " + status);
      }
    } catch (CancellationException ex) {
      throw new EventDeliveryException(this + ": RPC future was cancelled", ex);
    } catch (ExecutionException ex) {
      throw new EventDeliveryException(this + ": Exception thrown from " +
          "remote handler", ex);
    } catch (TimeoutException ex) {
      throw new EventDeliveryException(this + ": RPC request timed out", ex);
    } catch (InterruptedException ex) {
      Thread.currentThread().interrupt();
      throw new EventDeliveryException(this + ": RPC request interrupted", ex);
    }
  }

  /**
   * This method should always be used to change {@code connState} so we ensure
   * that invalid state transitions do not occur and that the {@code isIdle}
   * {@link Condition} variable gets signaled reliably.
   * Throws {@code IllegalStateException} when called to transition from CLOSED
   * to another state.
   * @param newState
   */
  private void setState(ConnState newState) {
    stateLock.lock();
    try {
      if (connState == ConnState.DEAD && connState != newState) {
        throw new IllegalStateException("Cannot transition from CLOSED state.");
      }
      connState = newState;
    } finally {
      stateLock.unlock();
    }
  }

  /**
   * If the connection state != READY, throws {@link EventDeliveryException}.
   */
  private void assertReady() throws EventDeliveryException {
    stateLock.lock();
    try {
      ConnState curState = connState;
      if (curState != ConnState.READY) {
        throw new EventDeliveryException("RPC failed, client in an invalid " +
            "state: " + curState);
      }
    } finally {
      stateLock.unlock();
    }
  }

  /**
   * Helper function to convert a map of String to a map of CharSequence.
   */
  private static Map<CharSequence, CharSequence> toCharSeqMap(
      Map<String, String> stringMap) {
    Map<CharSequence, CharSequence> charSeqMap =
        new HashMap<CharSequence, CharSequence>();
    for (Map.Entry<String, String> entry : stringMap.entrySet()) {
      charSeqMap.put(entry.getKey(), entry.getValue());
    }
    return charSeqMap;
  }

  @Override
  public boolean isActive() {
    stateLock.lock();
    try {
      return (connState == ConnState.READY);
    } finally {
      stateLock.unlock();
    }
  }

  private static enum ConnState {
    INIT, READY, DEAD
  }


  /**
   * <p>
   * Configure the actual client using the properties.
   * <tt>properties</tt> should have at least 2 params:
   * <p><tt>hosts</tt> = <i>alias_for_host</i></p>
   * <p><tt>alias_for_host</tt> = <i>hostname:port</i>. </p>
   * Only the first host is added, rest are discarded.</p>
   * <p>Optionally it can also have a <p>
   * <tt>batch-size</tt> = <i>batchSize</i>
   * @param properties The properties to instantiate the client with.
   * @return
   */
  @Override
  public synchronized void configure(Properties properties)
      throws FlumeException {
    stateLock.lock();
    try {
      if (connState == ConnState.READY || connState == ConnState.DEAD) {
        throw new FlumeException("This client was already configured, " +
            "cannot reconfigure.");
      }
    } finally {
      stateLock.unlock();
    }

    // batch size
    String strBatchSize = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_BATCH_SIZE);
    logger.debug("Batch size string = " + strBatchSize);
    batchSize = RpcClientConfigurationConstants.DEFAULT_BATCH_SIZE;
    if (strBatchSize != null && !strBatchSize.isEmpty()) {
      try {
        int parsedBatch = Integer.parseInt(strBatchSize);
        if (parsedBatch < 1) {
          logger.warn("Invalid value for batchSize: {}; Using default value.", parsedBatch);
        } else {
          batchSize = parsedBatch;
        }
      } catch (NumberFormatException e) {
        logger.warn("Batchsize is not valid for RpcClient: " + strBatchSize +
            ". Default value assigned.", e);
      }
    }

    // host and port
    String hostNames = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_HOSTS);
    String[] hosts = null;
    if (hostNames != null && !hostNames.isEmpty()) {
      hosts = hostNames.split("\\s+");
    } else {
      throw new FlumeException("Hosts list is invalid: " + hostNames);
    }

    if (hosts.length > 1) {
      logger.warn("More than one hosts are specified for the default client. "
          + "Only the first host will be used and others ignored. Specified: "
          + hostNames + "; to be used: " + hosts[0]);
    }

    String host = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_HOSTS_PREFIX + hosts[0]);
    if (host == null || host.isEmpty()) {
      throw new FlumeException("Host not found: " + hosts[0]);
    }
    String[] hostAndPort = host.split(":");
    if (hostAndPort.length != 2) {
      throw new FlumeException("Invalid hostname: " + hosts[0]);
    }
    Integer port = null;
    try {
      port = Integer.parseInt(hostAndPort[1]);
    } catch (NumberFormatException e) {
      throw new FlumeException("Invalid Port: " + hostAndPort[1], e);
    }
    this.address = new InetSocketAddress(hostAndPort[0], port);

    // connect timeout
    connectTimeout =
        RpcClientConfigurationConstants.DEFAULT_CONNECT_TIMEOUT_MILLIS;
    String strConnTimeout = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_CONNECT_TIMEOUT);
    if (strConnTimeout != null && strConnTimeout.trim().length() > 0) {
      try {
        connectTimeout = Long.parseLong(strConnTimeout);
        if (connectTimeout < 1000) {
          logger.warn("Connection timeout specified less than 1s. " +
              "Using default value instead.");
          connectTimeout =
              RpcClientConfigurationConstants.DEFAULT_CONNECT_TIMEOUT_MILLIS;
        }
      } catch (NumberFormatException ex) {
        logger.error("Invalid connect timeout specified: " + strConnTimeout);
      }
    }

    // request timeout
    requestTimeout =
        RpcClientConfigurationConstants.DEFAULT_REQUEST_TIMEOUT_MILLIS;
    String strReqTimeout = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_REQUEST_TIMEOUT);
    if  (strReqTimeout != null && strReqTimeout.trim().length() > 0) {
      try {
        requestTimeout = Long.parseLong(strReqTimeout);
        if (requestTimeout < 1000) {
          logger.warn("Request timeout specified less than 1s. " +
              "Using default value instead.");
          requestTimeout =
              RpcClientConfigurationConstants.DEFAULT_REQUEST_TIMEOUT_MILLIS;
        }
      } catch (NumberFormatException ex) {
        logger.error("Invalid request timeout specified: " + strReqTimeout);
      }
    }

    String enableCompressionStr =
        properties.getProperty(RpcClientConfigurationConstants.CONFIG_COMPRESSION_TYPE);
    if (enableCompressionStr != null && enableCompressionStr.equalsIgnoreCase("deflate")) {
      this.enableDeflateCompression = true;
      String compressionLvlStr =
          properties.getProperty(RpcClientConfigurationConstants.CONFIG_COMPRESSION_LEVEL);
      compressionLevel = RpcClientConfigurationConstants.DEFAULT_COMPRESSION_LEVEL;
      if (compressionLvlStr != null) {
        try {
          compressionLevel = Integer.parseInt(compressionLvlStr);
        } catch (NumberFormatException ex) {
          logger.error("Invalid compression level: " + compressionLvlStr);
        }
      }
    }

    enableSsl = Boolean.parseBoolean(properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_SSL));
    trustAllCerts = Boolean.parseBoolean(properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_TRUST_ALL_CERTS));
    truststore = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_TRUSTSTORE);
    truststorePassword = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_TRUSTSTORE_PASSWORD);
    truststoreType = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_TRUSTSTORE_TYPE, "JKS");
    String excludeProtocolsStr = properties.getProperty(
        RpcClientConfigurationConstants.CONFIG_EXCLUDE_PROTOCOLS);
    if (excludeProtocolsStr == null) {
      excludeProtocols.add("SSLv3");
    } else {
      excludeProtocols.addAll(Arrays.asList(excludeProtocolsStr.split(" ")));
      if (!excludeProtocols.contains("SSLv3")) {
        excludeProtocols.add("SSLv3");
      }
    }

    String maxIoWorkersStr = properties.getProperty(RpcClientConfigurationConstants.MAX_IO_WORKERS);
    if (!StringUtils.isEmpty(maxIoWorkersStr)) {
      try {
        maxIoWorkers = Integer.parseInt(maxIoWorkersStr);
      } catch (NumberFormatException ex) {
        logger.warn("Invalid maxIOWorkers:" + maxIoWorkersStr + " Using " +
            "default maxIOWorkers.");
        maxIoWorkers = -1;
      }
    }

    if (maxIoWorkers < 1) {
      logger.warn("Using default maxIOWorkers");
      maxIoWorkers = -1;
    }

    this.connect();
  }

  /**
   * A thread factor implementation modeled after the implementation of
   * NettyTransceiver.NettyTransceiverThreadFactory class which is
   * a private static class. The only difference between that and this
   * implementation is that this implementation marks all the threads daemon
   * which allows the termination of the VM when the non-daemon threads
   * are done.
   */
  private static class TransceiverThreadFactory implements ThreadFactory {
    private final AtomicInteger threadId = new AtomicInteger(0);
    private final String prefix;

    /**
     * Creates a TransceiverThreadFactory that creates threads with the
     * specified name.
     * @param prefix the name prefix to use for all threads created by this
     * ThreadFactory.  A unique ID will be appended to this prefix to form the
     * final thread name.
     */
    public TransceiverThreadFactory(String prefix) {
      this.prefix = prefix;
    }

    @Override
    public Thread newThread(Runnable r) {
      Thread thread = new Thread(r);
      thread.setDaemon(true);
      thread.setName(prefix + " " + threadId.incrementAndGet());
      return thread;
    }
  }

  /**
   * Factory of SSL-enabled client channels
   * Copied from Avro's org.apache.avro.ipc.TestNettyServerWithSSL test
   */
  private static class SSLCompressionChannelFactory extends NioClientSocketChannelFactory {

    private final boolean enableCompression;
    private final int compressionLevel;
    private final boolean enableSsl;
    private final boolean trustAllCerts;
    private final String truststore;
    private final String truststorePassword;
    private final String truststoreType;
    private final List<String> excludeProtocols;

    public SSLCompressionChannelFactory(Executor bossExecutor, Executor workerExecutor,
        boolean enableCompression, boolean enableSsl, boolean trustAllCerts,
        int compressionLevel, String truststore, String truststorePassword,
        String truststoreType, List<String> excludeProtocols) {
      super(bossExecutor, workerExecutor);
      this.enableCompression = enableCompression;
      this.enableSsl = enableSsl;
      this.compressionLevel = compressionLevel;
      this.trustAllCerts = trustAllCerts;
      this.truststore = truststore;
      this.truststorePassword = truststorePassword;
      this.truststoreType = truststoreType;
      this.excludeProtocols = excludeProtocols;
    }

    public SSLCompressionChannelFactory(Executor bossExecutor, Executor workerExecutor,
        boolean enableCompression, boolean enableSsl, boolean trustAllCerts,
        int compressionLevel, String truststore, String truststorePassword,
        String truststoreType, List<String> excludeProtocols, int maxIOWorkers) {
      super(bossExecutor, workerExecutor, maxIOWorkers);
      this.enableCompression = enableCompression;
      this.enableSsl = enableSsl;
      this.compressionLevel = compressionLevel;
      this.trustAllCerts = trustAllCerts;
      this.truststore = truststore;
      this.truststorePassword = truststorePassword;
      this.truststoreType = truststoreType;
      this.excludeProtocols = excludeProtocols;
    }

    @Override
    public SocketChannel newChannel(ChannelPipeline pipeline) {
      TrustManager[] managers;
      try {
        if (enableCompression) {
          ZlibEncoder encoder = new ZlibEncoder(compressionLevel);
          pipeline.addFirst("deflater", encoder);
          pipeline.addFirst("inflater", new ZlibDecoder());
        }
        if (enableSsl) {
          if (trustAllCerts) {
            logger.warn("No truststore configured, setting TrustManager to accept"
                + " all server certificates");
            managers = new TrustManager[] { new PermissiveTrustManager() };
          } else {
            KeyStore keystore = null;

            if (truststore != null) {
              if (truststorePassword == null) {
                throw new NullPointerException("truststore password is null");
              }
              InputStream truststoreStream = new FileInputStream(truststore);
              keystore = KeyStore.getInstance(truststoreType);
              keystore.load(truststoreStream, truststorePassword.toCharArray());
            }

            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            // null keystore is OK, with SunX509 it defaults to system CA Certs
            // see http://docs.oracle.com/javase/6/docs/technotes/guides/security/jsse/JSSERefGuide.html#X509TrustManager
            tmf.init(keystore);
            managers = tmf.getTrustManagers();
          }

          SSLContext sslContext = SSLContext.getInstance("TLS");
          sslContext.init(null, managers, null);
          SSLEngine sslEngine = sslContext.createSSLEngine();
          sslEngine.setUseClientMode(true);
          List<String> enabledProtocols = new ArrayList<String>();
          for (String protocol : sslEngine.getEnabledProtocols()) {
            if (!excludeProtocols.contains(protocol)) {
              enabledProtocols.add(protocol);
            }
          }
          sslEngine.setEnabledProtocols(enabledProtocols.toArray(new String[0]));
          logger.info("SSLEngine protocols enabled: " +
              Arrays.asList(sslEngine.getEnabledProtocols()));
          // addFirst() will make SSL handling the first stage of decoding
          // and the last stage of encoding this must be added after
          // adding compression handling above
          pipeline.addFirst("ssl", new SslHandler(sslEngine));
        }

        return super.newChannel(pipeline);
      } catch (Exception ex) {
        logger.error("Cannot create SSL channel", ex);
        throw new RuntimeException("Cannot create SSL channel", ex);
      }
    }
  }

  /**
   * Permissive trust manager accepting any certificate
   */
  private static class PermissiveTrustManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(X509Certificate[] certs, String s) {
      // nothing
    }

    @Override
    public void checkServerTrusted(X509Certificate[] certs, String s) {
      // nothing
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
      return new X509Certificate[0];
    }
  }
}
