org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer.java

Source

/****************************************************************
 * 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.james.protocols.lib.netty;

import java.io.FileInputStream;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.james.filesystem.api.FileSystem;
import org.apache.james.lifecycle.api.Configurable;
import org.apache.james.lifecycle.api.LogEnabled;
import org.apache.james.protocols.api.Encryption;
import org.apache.james.protocols.lib.jmx.ServerMBean;
import org.apache.james.protocols.netty.AbstractAsyncServer;
import org.apache.james.util.concurrent.JMXEnabledThreadPoolExecutor;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.slf4j.Logger;

/**
 * Abstract base class for Servers for all James Servers
 */
public abstract class AbstractConfigurableAsyncServer extends AbstractAsyncServer
        implements LogEnabled, Configurable, ServerMBean {
    /** The default value for the connection backlog. */
    public static final int DEFAULT_BACKLOG = 200;

    /** The default value for the connection timeout. */
    public static final int DEFAULT_TIMEOUT = 5 * 60;

    /** The name of the parameter defining the connection timeout. */
    private static final String TIMEOUT_NAME = "connectiontimeout";

    /** The name of the parameter defining the connection backlog. */
    private static final String BACKLOG_NAME = "connectionBacklog";

    /** The name of the parameter defining the service hello name. */
    public static final String HELLO_NAME = "helloName";

    public static final int DEFAULT_MAX_EXECUTOR_COUNT = 16;

    // By default, use the Sun X509 algorithm that comes with the Sun JCE
    // provider for SSL
    // certificates
    private static final String defaultX509algorithm = "SunX509";

    // The X.509 certificate algorithm
    private String x509Algorithm = defaultX509algorithm;

    private FileSystem fileSystem;

    private Logger logger;

    private boolean enabled;

    protected int connPerIP;

    private boolean useStartTLS;
    private boolean useSSL;

    protected int connectionLimit;

    private String helloName;

    private String keystore;

    private String secret;

    private Encryption encryption;

    protected String jmxName;

    private String[] enabledCipherSuites;

    private final ConnectionCountHandler countHandler = new ConnectionCountHandler();

    private ExecutionHandler executionHandler = null;

    private int maxExecutorThreads;

    private MBeanServer mbeanServer;

    @Inject
    public final void setFileSystem(FileSystem filesystem) {
        this.fileSystem = filesystem;
    }

    /**
     * @see org.apache.james.lifecycle.api.LogEnabled#setLog(org.slf4j.Logger)
     */
    public final void setLog(Logger logger) {
        this.logger = logger;
    }

    protected void registerMBean() {

        try {
            mbeanServer.registerMBean(this, new ObjectName(getMBeanName()));
        } catch (Exception e) {
            throw new RuntimeException("Unable to register mbean", e);
        }

    }

    protected void unregisterMBean() {
        try {
            mbeanServer.unregisterMBean(new ObjectName(getMBeanName()));
        } catch (Exception e) {
            throw new RuntimeException("Unable to unregister mbean", e);
        }

    }

    private String getMBeanName() {
        return "org.apache.james:type=server,name=" + jmxName;
    }

    /**
     * @see
     * org.apache.james.lifecycle.api.Configurable
     * #configure(org.apache.commons.configuration.HierarchicalConfiguration)
     */
    public final void configure(HierarchicalConfiguration config) throws ConfigurationException {

        enabled = config.getBoolean("[@enabled]", true);

        final Logger logger = getLogger();
        if (!enabled) {
            logger.info(getServiceType() + " disabled by configuration");
            return;
        }

        String listen[] = config.getString("bind", "0.0.0.0:" + getDefaultPort()).split(",");
        List<InetSocketAddress> bindAddresses = new ArrayList<InetSocketAddress>();
        for (String aListen : listen) {
            String bind[] = aListen.split(":");

            InetSocketAddress address;
            String ip = bind[0].trim();
            int port = Integer.parseInt(bind[1].trim());
            if (!ip.equals("0.0.0.0")) {
                try {
                    ip = InetAddress.getByName(ip).getHostName();
                } catch (UnknownHostException unhe) {
                    throw new ConfigurationException(
                            "Malformed bind parameter in configuration of service " + getServiceType(), unhe);
                }
            }
            address = new InetSocketAddress(ip, port);

            String infoBuffer = getServiceType() + " bound to: " + ip + ":" + port;
            logger.info(infoBuffer);

            bindAddresses.add(address);
        }
        setListenAddresses(bindAddresses.toArray(new InetSocketAddress[bindAddresses.size()]));

        jmxName = config.getString("jmxName", getDefaultJMXName());
        int ioWorker = config.getInt("ioWorkerCount", DEFAULT_IO_WORKER_COUNT);
        setIoWorkerCount(ioWorker);

        maxExecutorThreads = config.getInt("maxExecutorCount", DEFAULT_MAX_EXECUTOR_COUNT);

        configureHelloName(config);

        setTimeout(config.getInt(TIMEOUT_NAME, DEFAULT_TIMEOUT));

        StringBuilder infoBuffer = new StringBuilder(64).append(getServiceType())
                .append(" handler connection timeout is: ").append(getTimeout());
        logger.info(infoBuffer.toString());

        setBacklog(config.getInt(BACKLOG_NAME, DEFAULT_BACKLOG));

        infoBuffer = new StringBuilder(64).append(getServiceType()).append(" connection backlog is: ")
                .append(getBacklog());
        logger.info(infoBuffer.toString());

        String connectionLimitString = config.getString("connectionLimit", null);
        if (connectionLimitString != null) {
            try {
                connectionLimit = new Integer(connectionLimitString);
            } catch (NumberFormatException nfe) {
                logger.error("Connection limit value is not properly formatted.", nfe);
            }
            if (connectionLimit < 0) {
                logger.error("Connection limit value cannot be less than zero.");
                throw new ConfigurationException("Connection limit value cannot be less than zero.");
            } else if (connectionLimit > 0) {
                infoBuffer = new StringBuilder(128).append(getServiceType()).append(" will allow a maximum of ")
                        .append(connectionLimitString).append(" connections.");
                logger.info(infoBuffer.toString());
            }
        }

        String connectionLimitPerIP = config.getString("connectionLimitPerIP", null);
        if (connectionLimitPerIP != null) {
            try {
                connPerIP = Integer.parseInt(connectionLimitPerIP);
            } catch (NumberFormatException nfe) {
                logger.error("Connection limit per IP value is not properly formatted.", nfe);
            }
            if (connPerIP < 0) {
                logger.error("Connection limit per IP value cannot be less than zero.");
                throw new ConfigurationException("Connection limit value cannot be less than zero.");
            } else if (connPerIP > 0) {
                infoBuffer = new StringBuilder(128).append(getServiceType()).append(" will allow a maximum of ")
                        .append(connPerIP).append(" per IP connections for ").append(getServiceType());
                logger.info(infoBuffer.toString());
            }
        }

        useStartTLS = config.getBoolean("tls.[@startTLS]", false);
        useSSL = config.getBoolean("tls.[@socketTLS]", false);

        if (useSSL && useStartTLS)
            throw new ConfigurationException("startTLS is only supported when using plain sockets");

        if (useStartTLS || useSSL) {
            enabledCipherSuites = config.getStringArray("tls.supportedCipherSuites.cipherSuite");
            keystore = config.getString("tls.keystore", null);
            if (keystore == null) {
                throw new ConfigurationException("keystore needs to get configured");
            }
            secret = config.getString("tls.secret", "");
            x509Algorithm = config.getString("tls.algorithm", defaultX509algorithm);
        }

        doConfigure(config);

    }

    @PostConstruct
    public final void init() throws Exception {

        if (isEnabled()) {

            buildSSLContext();
            preInit();
            executionHandler = createExecutionHander();
            bind();

            mbeanServer = ManagementFactory.getPlatformMBeanServer();
            registerMBean();

            getLogger().info("Init " + getServiceType() + " done");

        }

    }

    @PreDestroy
    public final void destroy() {

        getLogger().info("Dispose " + getServiceType());

        if (isEnabled()) {
            unbind();
            postDestroy();

            if (executionHandler != null) {
                executionHandler.releaseExternalResources();
            }

            unregisterMBean();
        }
        getLogger().info("Dispose " + getServiceType() + " done");

    }

    protected void postDestroy() {
        // override me
    }

    /**
     * This method is called on init of the Server. Subclasses should override
     * this method to init stuff
     * 
     * @throws Exception
     */
    protected void preInit() throws Exception {
        // override me
    }

    protected void doConfigure(HierarchicalConfiguration config) throws ConfigurationException {
        // override me
    }

    /**
     * Return the FileSystem
     * 
     * @return fileSystem
     */
    protected FileSystem getFileSystem() {
        return fileSystem;
    }

    /**
     * Configure the helloName for the given Configuration
     * 
     * @param handlerConfiguration
     * @throws ConfigurationException
     */
    protected void configureHelloName(Configuration handlerConfiguration) throws ConfigurationException {
        StringBuilder infoBuffer;
        String hostName;
        try {
            hostName = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException ue) {
            hostName = "localhost";
        }

        infoBuffer = new StringBuilder(64).append(getServiceType()).append(" is running on: ").append(hostName);
        getLogger().info(infoBuffer.toString());

        boolean autodetect = handlerConfiguration.getBoolean(HELLO_NAME + ".[@autodetect]", true);
        if (autodetect) {
            helloName = hostName;
        } else {
            helloName = handlerConfiguration.getString(HELLO_NAME);
            if (helloName == null || helloName.trim().length() < 1) {
                throw new ConfigurationException("Please configure the helloName or use autodetect");
            }
        }

        infoBuffer = new StringBuilder(64).append(getServiceType()).append(" handler hello name is: ")
                .append(helloName);
        getLogger().info(infoBuffer.toString());
    }

    /**
     * Return the logger
     * 
     * @return logger
     */
    protected Logger getLogger() {
        return logger;
    }

    /**
     * Return if the server is enabled by the configuration
     * 
     * @return enabled
     */
    public boolean isEnabled() {
        return enabled;
    }

    /**
     * Return helloName for this server
     * 
     * @return helloName
     */
    public String getHelloName() {
        return helloName;
    }

    protected Encryption getEncryption() {
        return encryption;
    }

    /**
     * Build the SSLEngine
     * 
     * @throws Exception
     */

    private void buildSSLContext() throws Exception {
        if (useStartTLS || useSSL) {
            FileInputStream fis = null;
            try {
                KeyStore ks = KeyStore.getInstance("JKS");
                fis = new FileInputStream(fileSystem.getFile(keystore));
                ks.load(fis, secret.toCharArray());

                // Set up key manager factory to use our key store
                KeyManagerFactory kmf = KeyManagerFactory.getInstance(x509Algorithm);
                kmf.init(ks, secret.toCharArray());

                // Initialize the SSLContext to work with our key managers.
                SSLContext context = SSLContext.getInstance("TLS");
                context.init(kmf.getKeyManagers(), null, null);
                if (useStartTLS) {
                    encryption = Encryption.createStartTls(context, enabledCipherSuites);
                } else {
                    encryption = Encryption.createTls(context, enabledCipherSuites);
                }
            } finally {
                if (fis != null) {
                    fis.close();
                }
            }
        }
    }

    /**
     * Return the default port which will get used for this server if non is
     * specify in the configuration
     * 
     * @return port
     */
    protected abstract int getDefaultPort();

    /**
     * Return the socket type. The Socket type can be secure or plain
     * 
     * @return the socket type ('plain' or 'secure')
     */
    public String getSocketType() {
        if (encryption != null && !encryption.isStartTLS()) {
            return "secure";
        }
        return "plain";
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#getStartTLSSupported()
     */
    public boolean getStartTLSSupported() {
        return encryption != null && encryption.isStartTLS();
    }

    /**
     * @see
     * org.apache.james.protocols.lib.jmx.ServerMBean#getMaximumConcurrentConnections()
     */
    public int getMaximumConcurrentConnections() {
        return connectionLimit;
    }

    protected String getThreadPoolJMXPath() {
        return "org.apache.james:type=server,name=" + jmxName + ",sub-type=threadpool";
    }

    @Override
    protected Executor createBossExecutor() {
        return JMXEnabledThreadPoolExecutor.newCachedThreadPool(getThreadPoolJMXPath(),
                getDefaultJMXName() + "-boss");
    }

    @Override
    protected Executor createWorkerExecutor() {
        return JMXEnabledThreadPoolExecutor.newCachedThreadPool(getThreadPoolJMXPath(),
                getDefaultJMXName() + "-worker");
    }

    /**
     * Return the default name of the the server in JMX if none is configured
     * via "jmxname" in the configuration
     * 
     * @return defaultJmxName
     */
    protected abstract String getDefaultJMXName();

    protected String[] getEnabledCipherSuites() {
        return enabledCipherSuites;
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#isStarted()
     */
    public boolean isStarted() {
        return isBound();
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#start()
     */
    public boolean start() {
        try {
            bind();
        } catch (Exception e) {
            logger.error("Unable to start server");
            return false;
        }
        return true;
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#stop()
     */
    public boolean stop() {
        unbind();
        return true;
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#getHandledConnections()
     */
    public long getHandledConnections() {
        return countHandler.getConnectionsTillStartup();
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#getCurrentConnections()
     */
    public int getCurrentConnections() {
        return countHandler.getCurrentConnectionCount();
    }

    protected ConnectionCountHandler getConnectionCountHandler() {
        return countHandler;
    }

    /**
     * @see org.apache.james.protocols.lib.jmx.ServerMBean#getBoundAddresses()
     */
    public String[] getBoundAddresses() {

        List<InetSocketAddress> addresses = getListenAddresses();
        String[] addrs = new String[addresses.size()];
        for (int i = 0; i < addresses.size(); i++) {
            InetSocketAddress address = addresses.get(i);
            addrs[i] = address.getHostName() + ":" + address.getPort();
        }

        return addrs;
    }

    @Override
    protected void configureBootstrap(ServerBootstrap bootstrap) {
        super.configureBootstrap(bootstrap);

        // enable tcp keep-alives
        bootstrap.setOption("child.keepAlive", true);
    }

    /**
     * Create a new {@link ExecutionHandler} which is used to execute IO-Bound handlers
     * 
     * @return ehandler
     */
    protected ExecutionHandler createExecutionHander() {
        return new ExecutionHandler(new JMXEnabledOrderedMemoryAwareThreadPoolExecutor(maxExecutorThreads, 0, 0,
                getThreadPoolJMXPath(), getDefaultJMXName() + "-executor"));
    }

    /**
     * Return the {@link ExecutionHandler} or null if non should be used. Be sure you call {@link #createExecutionHander()} before
     * 
     * @return ehandler
     */
    protected ExecutionHandler getExecutionHandler() {
        return executionHandler;
    }

    protected abstract ChannelUpstreamHandler createCoreHandler();

    @Override
    protected ChannelPipelineFactory createPipelineFactory(ChannelGroup group) {
        return new AbstractExecutorAwareChannelPipelineFactory(getTimeout(), connectionLimit, connPerIP, group,
                enabledCipherSuites, getExecutionHandler()) {
            @Override
            protected SSLContext getSSLContext() {
                if (encryption == null) {
                    return null;
                } else {
                    return encryption.getContext();
                }
            }

            @Override
            protected boolean isSSLSocket() {
                return encryption != null && !encryption.isStartTLS();
            }

            @Override
            protected ChannelUpstreamHandler createHandler() {
                return AbstractConfigurableAsyncServer.this.createCoreHandler();

            }

            @Override
            protected ConnectionCountHandler getConnectionCountHandler() {
                return AbstractConfigurableAsyncServer.this.getConnectionCountHandler();
            }

        };
    }

}