org.apache.james.mpt.DiscardProtocol.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.mpt.DiscardProtocol.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.mpt;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Queue;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Simple <a href='http://tools.ietf.org/html/rfc863'>RFC 863</a> implementation.
 */
public class DiscardProtocol {

    private static final Charset ASCII = Charset.forName("US-ASCII");

    private static final int SOCKET_CONNECTION_WAIT_MILLIS = 30;

    private static final int IDLE_TIMEOUT = 120000;

    private static final Log LOG = LogFactory.getLog(DiscardProtocol.class);

    /** Serve on this port */
    private final int port;

    /** 
     * Queues requests for recordings.
     * Also, used as lock.
     */
    private final Queue<Server> queue;

    private final Collection<Server> runningServers;

    /** 
     * Server socket when started, null otherwise.
     * Null indicates to the socket serving thread that the server is stopped.
     */
    private volatile ServerSocketChannel socket;

    public DiscardProtocol(int port) {
        super();
        this.port = port;
        queue = new LinkedList<Server>();
        runningServers = new LinkedList<Server>();
    }

    /**
     * Starts serving.
     * @throws IOException when connection fails
     * @throws IllegalStateException when already started
     */
    public void start() throws IOException {
        synchronized (queue) {
            if (socket == null) {
                socket = ServerSocketChannel.open();
                socket.socket().bind(new InetSocketAddress(port));
                // only going to record a single conversation
                socket.configureBlocking(false);

                final Thread socketMonitorThread = new Thread(new SocketMonitor());
                socketMonitorThread.start();

            } else {
                throw new IllegalStateException("Already started");
            }
        }
    }

    public Record recordNext() {
        synchronized (queue) {
            Server server = new Server();
            queue.add(server);
            return server;
        }
    }

    private void abort() {
        synchronized (queue) {
            stop();
            for (Server server : queue) {
                server.abort();
            }
            queue.clear();
        }
    }

    /**
     * Stops serving.
     * @return ASCII bytes sent to socket by first
     */
    public void stop() {
        synchronized (queue) {
            try {
                if (socket != null) {
                    if (socket.isOpen()) {
                        socket.close();
                    }
                }
            } catch (IOException e) {
                LOG.warn("Failed to close socket", e);
            }
            socket = null;
            for (Server server : runningServers) {
                server.abort();
            }
        }
    }

    private final class SocketMonitor implements Runnable {
        public void run() {
            try {
                long lastConnection = System.currentTimeMillis();
                while (socket != null) {
                    final SocketChannel socketChannel = socket.accept();
                    if (socketChannel == null) {
                        if (System.currentTimeMillis() - lastConnection > IDLE_TIMEOUT) {
                            throw new Exception("Idle timeout");
                        }
                        Thread.sleep(SOCKET_CONNECTION_WAIT_MILLIS);
                    } else {
                        synchronized (queue) {
                            Server nextServer = (Server) queue.poll();
                            if (nextServer == null) {
                                nextServer = new Server();
                            }
                            nextServer.setSocketChannel(socketChannel);

                            final Thread channelThread = new Thread(nextServer);
                            channelThread.start();
                            runningServers.add(nextServer);
                            lastConnection = System.currentTimeMillis();
                        }
                    }
                }
            } catch (Exception e) {
                LOG.fatal("Cannot accept connection", e);
                abort();
            }
        }
    }

    public interface Record {
        /** Blocks until completion of conversation */
        String complete() throws Exception;
    }

    /**
     * Basic server.
     */
    private final static class Server implements Runnable, Record {

        private static final int COMPLETION_TIMEOUT = 60000;

        private static final int COMPLETION_PAUSE = 1000;

        private static final int INITIAL_BUFFER_CAPACITY = 2048;

        private final ByteBuffer buffer;
        /**
         * Safe for concurrent access by multiple threads.
         */
        private final StringBuffer out;

        /**
         * Initialised by setter 
         */
        private SocketChannel socketChannel;

        private volatile boolean aborted;
        private volatile boolean complete;

        public Server() {
            complete = false;
            out = new StringBuffer(INITIAL_BUFFER_CAPACITY);
            buffer = ByteBuffer.allocate(INITIAL_BUFFER_CAPACITY);
            aborted = false;
            socketChannel = null;
        }

        public void setSocketChannel(SocketChannel socketChannel) {
            this.socketChannel = socketChannel;
        }

        public void run() {
            try {
                if (socketChannel == null) {
                    LOG.fatal("Socket channel must be set before instance is run.");
                } else {
                    try {
                        while (!socketChannel.finishConnect()) {
                            Thread.sleep(SOCKET_CONNECTION_WAIT_MILLIS);
                        }

                        int read = 0;
                        while (!aborted && socketChannel.isOpen() && read >= 0) {
                            read = socketChannel.read(buffer);
                            if (!buffer.hasRemaining()) {
                                decant();
                            }
                        }

                    } catch (Exception e) {
                        LOG.fatal("Socket communication failed", e);
                        aborted = true;

                        // Tidy up
                    } finally {
                        try {
                            socketChannel.close();
                        } catch (Exception e) {
                            LOG.debug("Ignoring failure to close socket.", e);
                        }
                    }
                }
            } finally {
                synchronized (this) {
                    // Ensure completion is flagged
                    complete = true;
                    // Signal to any waiting threads 
                    notifyAll();
                }
            }
        }

        /**
         * Transfers all data from buffer to builder
         *
         */
        private void decant() {
            buffer.flip();
            final CharBuffer decoded = ASCII.decode(buffer);
            out.append(decoded);
            buffer.clear();
        }

        public void abort() {
            aborted = true;
        }

        /**
         * Blocks until connection is complete (closed)
         */
        public synchronized String complete() throws Exception {
            if (aborted) {
                throw new Exception("Aborted");
            }
            final long startTime = System.currentTimeMillis();
            boolean isTimedOut = false;
            while (!complete && !isTimedOut) {
                wait(COMPLETION_PAUSE);
                isTimedOut = (System.currentTimeMillis() - startTime) > COMPLETION_TIMEOUT;
            }
            if (isTimedOut && !complete) {
                throw new Exception("Timed out wait for be notified that read is complete");
            }
            decant();
            return out.toString();
        }
    }
}