de.fu_berlin.inf.dpp.net.internal.StreamSession.java Source code

Java tutorial

Introduction

Here is the source code for de.fu_berlin.inf.dpp.net.internal.StreamSession.java

Source

/*
 * DPP - Serious Distributed Pair Programming
 * (c) Freie Universitt Berlin - Fachbereich Mathematik und Informatik - 2010
 * (c) Stephan Lau - 2010
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 1, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package de.fu_berlin.inf.dpp.net.internal;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.log4j.Logger;
import org.picocontainer.Disposable;

import com.Ostermiller.util.CircularByteBuffer;

import de.fu_berlin.inf.dpp.exceptions.StreamException;
import de.fu_berlin.inf.dpp.net.JID;
import de.fu_berlin.inf.dpp.net.internal.StreamServiceManager.StreamPacket;
import de.fu_berlin.inf.dpp.net.internal.StreamServiceManager.StreamPath;
import de.fu_berlin.inf.dpp.util.ThreadAccessRecorder;
import de.fu_berlin.inf.dpp.util.Utils;

/**
 * This is a concrete session, based on a {@link StreamService}. It contains all
 * available streams and methods to handle the life-cycle of this session.
 * 
 * @author s-lau
 */
public class StreamSession implements Disposable {

    private static Logger log = Logger.getLogger(StreamSession.class);

    protected StreamServiceManager streamServiceManager;
    protected StreamSessionListener sessionListener = null;

    /**
     * Service of this session
     */
    protected StreamService basedService;

    /**
     * The path for this session to send meta-packets
     */
    protected StreamPath streamMetaPath;

    /**
     * Unique session ID (in combination with initiator)
     */
    protected int sessionID;

    /**
     * who started this session
     */
    protected JID initiator;

    /**
     * who receives our data etc.
     */
    protected JID remoteJID;

    /**
     * initial object passed with the negotiation
     */
    protected Object initiationDescription;

    /**
     * All available {@link InputStream}s
     */
    protected StreamSessionInputStream[] inputStreams;
    /**
     * All available {@link OutputStream}s
     */
    protected StreamSessionOutputStream[] outputStreams;

    /**
     * For each {@link StreamSessionOutputStream} a possible thread which sleeps
     * {@link StreamService#getMaximumDelay()} and then forces the data to be
     * send. It is started when a stream contains less data than it's defined
     * chunk-size.
     * 
     * @see StreamService#getChunkSize()
     */
    protected Thread[] resendThread;
    protected Runnable shutdown = null;

    /**
     * will be set to <code>true</code> when receiver send a STOPPED-packet (his
     * shutdown finished).
     */
    protected boolean receiverStopped = false;

    /**
     * We stopped this session, acknowledged shutdown by
     * {@link #shutdownFinished()}
     */
    protected boolean stopped = false;

    protected boolean disposed = false;

    /**
     * Create a new {@link StreamSession}
     * 
     * @param service
     *            this session is based on
     * @param receiver
     *            remote peer of this session (who receives the data)
     * @param initiator
     *            user who initiated this session
     * @param initial
     *            can be <code>null</code>
     */
    protected StreamSession(StreamServiceManager streamServiceManager, StreamService service, JID receiver,
            JID initiator, int sessionID, Object initial) {
        this.remoteJID = receiver;
        this.initiator = initiator;
        this.sessionID = sessionID;
        this.basedService = service;
        this.streamMetaPath = new StreamPath(initiator, getService(), sessionID);
        this.streamServiceManager = streamServiceManager;
        this.initiationDescription = initial;
        this.resendThread = new Thread[basedService.getStreamsPerSession()];

        // setup streams
        int streams = service.getStreamsPerSession();
        inputStreams = new StreamSessionInputStream[streams];
        outputStreams = new StreamSessionOutputStream[streams];

        for (int i = 0; i < streams; i++) {
            int bufferSize = basedService.getBufferSize()[i];

            inputStreams[i] = new StreamSessionInputStream(i, bufferSize);
            outputStreams[i] = new StreamSessionOutputStream(i, bufferSize);

        }
    }

    public StreamService getService() {
        return basedService;
    }

    /**
     * 
     * @return a META-path for this session
     */
    protected StreamPath getStreamPath() {
        return streamMetaPath;
    }

    /**
     * @return a {@link TransferDescription} for sending meta-packets to
     *         {@link #remoteJID}
     */
    protected TransferDescription getTransferDescription() {
        return TransferDescription.createStreamMetaTransferDescription(remoteJID,
                streamServiceManager.saros.getMyJID(), getStreamPath().toString(),
                streamServiceManager.sarosSessionID.getValue());
    }

    public JID getRemoteJID() {
        return remoteJID;
    }

    /**
     * Access to the {@link OutputStream}'s of this session
     * 
     * @param streamID
     *            ID of desired stream
     * @return stream or <code>null</code> if there's no such stream with given
     *         ID
     */
    public OutputStream getOutputStream(int streamID) {
        return streamID < 0 || streamID >= getService().getStreamsPerSession() ? null : outputStreams[streamID];
    }

    /**
     * Access to the {@link InputStream}'s of this session
     * 
     * @param streamID
     *            ID of desired stream
     * @return stream or <code>null</code> if there's no such stream with given
     *         ID
     */
    public InputStream getInputStream(int streamID) {
        return streamID < 0 || streamID >= getService().getStreamsPerSession() ? null : inputStreams[streamID];
    }

    /**
     * 
     * @return sessionID of this session
     */
    public int getSessionID() {
        return this.sessionID;
    }

    /**
     * Terminates this running session. The receiver will be notified. After
     * that the registered listener's method
     * {@link StreamSessionListener#sessionStopped()} will be called to shut our
     * session down.
     */
    public void stopSession() {
        streamServiceManager.stopSession(this);

        if (sessionListener == null)
            // immediately stop when no listener registered
            shutdownFinished();
    }

    /**
     * closes the session and it's streams
     */
    public synchronized void dispose() {
        if (disposed)
            return;
        disposed = true;
        // close streams
        closeStreams(inputStreams);
        closeStreams(outputStreams);
        // shutdown threads
        for (Thread t : resendThread) {
            if (t != null)
                t.interrupt();
        }
        if (streamServiceManager.sender != null)
            streamServiceManager.sender.removeData(this);
        streamServiceManager.sessions.remove(this.getStreamPath());
    }

    private void closeStreams(Stream[] streams) {
        for (Stream s : streams) {
            s.closedByInternal();
        }
    }

    /**
     * Notifies our {@link #streamServiceManager} that we are finished with
     * shutdown. Should only be called after a stop request
     * {@link StreamSessionListener#sessionStopped()}.
     */
    public void shutdownFinished() {
        streamServiceManager.stoppedSession(this);
        this.stopped = true;
    }

    /**
     * 
     * @return <code>true</code> when we are initiator of this session
     */
    public boolean isInitiator() {
        return this.initiator.equals(streamServiceManager.saros.getMyJID());
    }

    /**
     * @return The {@link Object} passed with the initiation for this session.
     */
    public Object getInitiationDescription() {
        return initiationDescription;
    }

    @Override
    public String toString() {
        return remoteJID + "[" + initiator + "-" + sessionID + "] (" + getService().getServiceName() + ")";
    }

    /**
     * Register a listener to this session. There can only be one listener at a
     * time. If session is already stopped when listener is added, it will be
     * notified. If session is disposed or stopped when listener is added, an
     * error is notified.
     */
    public void setListener(StreamSessionListener listener) {
        sessionListener = listener;

        if (sessionListener != null) {
            if (stopped)
                sessionListener.sessionStopped();
        }
    }

    protected void addPacket(StreamPacket packet) {
        int streamID = packet.getStreamPath().streamID;
        if (streamID >= getService().getStreamsPerSession()) {
            log.error("Received packet for unknown streamID #" + streamID + "! " + getService() + " contains only "
                    + getService().getStreamsPerSession());
            return;
        }

        inputStreams[streamID].addPacket(packet);
    }

    /**
     * Reports an error to sessions listener and disposes this session.
     * 
     * @param e
     */
    protected void reportErrorAndDispose(StreamException e) {
        if (sessionListener != null)
            sessionListener.errorOccured(e);
        this.dispose();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((initiator == null) ? 0 : initiator.hashCode());
        result = prime * result + sessionID;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        StreamSession other = (StreamSession) obj;
        if (initiator == null) {
            if (other.initiator != null)
                return false;
        } else if (!initiator.equals(other.initiator))
            return false;
        if (sessionID != other.sessionID)
            return false;
        return true;
    }

    /**
     * This listener provides call-backs for termination of a session. A session
     * can be terminated by either an explicit stop-request of one participant
     * or an error.
     */
    public interface StreamSessionListener {

        /**
         * A session shutdown was requested (by remote or ourself). This means
         * we have to shutdown this session now within
         * {@link StreamServiceManager#SESSION_SHUTDOWN_LIMIT} and call
         * {@link StreamSession#shutdownFinished()} to signal finished shutdown.
         */
        public void sessionStopped();

        /**
         * Report an error in the session/connection. This session will be
         * disposed when this call returns.
         * 
         * @param e
         *            The exception which caused an error
         */
        public void errorOccured(StreamException e);
    }

    /**
     * Getter and closing methods for streams, no matter they are in or out.
     */
    interface Stream extends Closeable {
        /**
         * 
         * @return {@link StreamSession} this stream belongs to
         */
        public StreamSession getSession();

        /**
         * Getter for streams ID
         * 
         * @return
         */
        public int streamID();

        /**
         * Other side of this stream is closed. This means that this stream is
         * now closed as well, because it makes no sense to leave it open.
         */
        public void closedByRemote();

        /**
         * Closes the stream without notifying related one (at receiver). Should
         * only be used internally.
         */
        public void closedByInternal();

        /**
         * 
         * @return <code>true</code> when stream is closed, <code>false</code>
         *         otherwise
         */
        public boolean isClosed();
    }

    /**
     * Virtual {@link OutputStream} for sending data to related
     * {@link StreamSessionInputStream} at {@link StreamSession#remoteJID}.
     */
    public class StreamSessionOutputStream extends OutputStream implements Stream {
        // TODO include SubMonitor!?

        protected ByteArrayOutputStream output;
        protected int streamID;
        /**
         * Related {@link StreamSessionInputStream} is closed
         */
        protected boolean closedByRemote = false;
        /**
         * this stream is closed
         */
        protected boolean closed = false;
        /**
         * Remaining buffer in this stream. Limits the capacity of
         * {@link #output}.
         */
        protected Semaphore freeBuffer;
        protected int bufferSize;

        protected ThreadAccessRecorder threadAccessRecorder = new ThreadAccessRecorder();

        protected StreamSessionOutputStream(int streamID, int bufferSize) {
            super();
            this.streamID = streamID;
            this.output = new ByteArrayOutputStream(bufferSize);
            this.freeBuffer = new Semaphore(bufferSize, true);
            this.bufferSize = bufferSize;
        }

        public void closedByRemote() {
            closedByRemote = true;
        }

        @Override
        public void write(int b) throws IOException {
            checkState();

            try {
                threadAccessRecorder.record();
                freeBuffer.acquire(1);
                synchronized (output) {
                    output.write(b);
                }
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    throw new InterruptedIOException();
                }
            }

            notifyStreamServiceManager(false);
        }

        /**
         * Check state of stream and throw {@link IOException} when necessary
         * 
         * @throws IOException
         *             when this stream itself or sink is is closed
         * @see #close()
         */
        protected void checkState() throws IOException {
            if (closed)
                throw new IOException("This stream is closed.");
            if (closedByRemote)
                throw new IOException("Sink has been closed.");
        }

        @Override
        public synchronized void close() throws IOException {
            if (closed)
                return;
            streamServiceManager.closeStream(this);
            closedByInternal();
        }

        public void closedByInternal() {
            if (closed)
                return;
            this.threadAccessRecorder.interrupt();
            closed = true;
            // only close empty buffer
            if (output.size() == 0)
                disposeBuffer();
        }

        protected void disposeBuffer() {
            if (output == null)
                return;

            try {
                output.close();
            } catch (IOException e) {
                // ignore
            }
            output = null;
        }

        @Override
        public void flush() throws IOException {
            checkState();
            output.flush();
            notifyStreamServiceManager(true);
        }

        @Override
        public void write(byte[] arg0) throws IOException {
            checkState();

            try {
                threadAccessRecorder.record();
                freeBuffer.acquire(arg0.length);
                synchronized (output) {
                    output.write(arg0);
                }
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    throw new InterruptedIOException();
                }
            }

            notifyStreamServiceManager(false);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            checkState();

            try {
                threadAccessRecorder.record();
                freeBuffer.acquire(len - off);
                synchronized (output) {
                    output.write(b, off, len);
                }
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    throw new InterruptedIOException();
                }
            }

            notifyStreamServiceManager(false);
        }

        /**
         * Gets the data this stream contains. The returned data will be removed
         * from this stream.
         * 
         * @param removeAllAvailableData
         *            get any amount of data without restrictions streams
         *            minimal chunk-size
         * @return the data in this stream, <code>null</code> when none
         *         available
         * @see StreamService#getChunkSize()
         */
        protected synchronized byte[] getData(boolean removeAllAvailableData) {
            if (output == null)
                return null;
            synchronized (output) {
                int data_length = output.size();
                if (data_length == 0)
                    // no data, nothing to do
                    return null;
                if (!removeAllAvailableData) {
                    // check if we should send data
                    if (data_length < StreamSession.this.getService().getChunkSize()[streamID]) {
                        // not enough data, spawn resendThread and return
                        if (StreamSession.this.resendThread[streamID] != null)
                            // another one waits already
                            return null;
                        StreamSession.this.resendThread[streamID] = Utils.runSafeAsync(
                                StreamSession.this.toString() + "-dataWaiter-" + streamID, log, new Runnable() {
                                    public void run() {
                                        try {
                                            Thread.sleep(
                                                    StreamSession.this.getService().getMaximumDelay()[streamID]);
                                            notifyStreamServiceManager(true);
                                        } catch (InterruptedException e) {
                                            // stop execution
                                            return;
                                        } finally {
                                            StreamSession.this.resendThread[streamID] = null;
                                        }
                                    }
                                });
                        return null;
                    }
                }
                byte[] data = output.toByteArray();
                log.trace("Will read " + Utils.formatByte(data.length));
                output.reset();
                if (StreamSession.this.resendThread[streamID] != null)
                    StreamSession.this.resendThread[streamID].interrupt();
                freeBuffer.release(data.length);

                if (closed)
                    disposeBuffer();

                return data;
            }
        }

        /**
         * Reports that data was written to this stream. The
         * {@link StreamSession#streamServiceManager} will be notified.
         * 
         * @param sendAllAvailableData
         *            when <code>true</code> any data in this stream will be
         *            send without restrictions.
         */
        protected void notifyStreamServiceManager(boolean sendAllAvailableData) {
            streamServiceManager.notifyDataAvailable(this, sendAllAvailableData, null);
        }

        public StreamSession getSession() {
            return StreamSession.this;
        }

        public int streamID() {
            return streamID;
        }

        protected StreamPath getStreamPath(int size) {
            return new StreamPath(initiator, sessionID, streamID, size);
        }

        public int getFreeBuffer() {
            return freeBuffer.availablePermits();
        }

        public int getTotalBuffer() {
            return bufferSize;
        }

        public boolean isClosed() {
            return closed || closedByRemote;
        }

    }

    /**
     * Virtual {@link InputStream} for accessing arrived data in this
     * {@link StreamSession}. When related {@link StreamSessionOutputStream} is
     * closed (at {@link StreamSession#remoteJID}), remaining bytes can be read
     * until end of stream is signaled.
     */
    public class StreamSessionInputStream extends InputStream implements Stream {
        // TODO include SubMonitor!?

        /**
         * Incoming packets which are not written to {@link #buffer} yet
         */
        protected Queue<StreamServiceManager.StreamPacket> remainingPackets = new LinkedList<StreamServiceManager.StreamPacket>();

        /**
         * Buffers data this stream contains
         */
        protected CircularByteBuffer buffer;

        protected int streamID;

        /**
         * Related {@link StreamSessionOutputStream} is closed
         */
        protected boolean closedByRemote = false;

        /**
         * this stream is closed
         */
        protected boolean closed = false;

        protected int readBytes = 0;

        protected ThreadAccessRecorder threadAccessRecorder = new ThreadAccessRecorder();

        protected StreamSessionInputStream(int streamID, int bufferSize) {
            this.streamID = streamID;
            // +10 because it uses some space for internal markers
            this.buffer = new CircularByteBuffer(bufferSize + 10, true);

        }

        public void closedByRemote() {
            closedByRemote = true;
            try {
                if (remainingPackets.isEmpty() && available() == 0)
                    threadAccessRecorder.interrupt();
            } catch (IOException e) {
                threadAccessRecorder.interrupt();
            }
        }

        /**
         * 
         * @return Read bytes since start of stream or last call to this method.
         */
        public synchronized int getReadBytes() {
            int t = readBytes;
            readBytes = 0;
            return t;
        }

        @Override
        public int read() throws IOException {
            checkState();
            try {
                if (closedByRemote && available() == 0)
                    return -1;
                threadAccessRecorder.record();
                fillBuffer();
                final int readByte = buffer.getInputStream().read();
                readBytes += 1;
                return readByte;
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } catch (IOException e) {
                if (closedByRemote) {
                    // closed while trying to read
                    return -1;
                }
                throw e;
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    if (!closedByRemote)
                        throw new InterruptedIOException();
                }
            }
        }

        @Override
        public int available() throws IOException {
            return buffer.getInputStream().available();
        }

        @Override
        public synchronized void close() throws IOException {
            if (closed)
                return;

            streamServiceManager.closeStream(this);
            closedByInternal();
            closed = true;
        }

        public void closedByInternal() {
            if (closed)
                return;

            this.closed = true;
            this.threadAccessRecorder.interrupt();

            if (remainingPackets != null) {
                for (StreamPacket p : remainingPackets) {
                    try {
                        p.reject();
                    } catch (IOException e) {
                        // connection broken, don't need to cancel rest
                        break;
                    }
                }
                remainingPackets.clear();
            }
        }

        @Override
        public boolean markSupported() {
            return buffer.getInputStream().markSupported();
        }

        @Override
        public int read(byte[] arg0) throws IOException {
            checkState();

            try {
                if (closedByRemote && available() == 0)
                    return -1;
                threadAccessRecorder.record();
                fillBuffer();
                final int read = buffer.getInputStream().read(arg0);
                readBytes += read;
                return read;
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } catch (IOException e) {
                if (closedByRemote) {
                    // closed while trying to read
                    return -1;
                }
                throw e;
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    if (!closedByRemote)
                        throw new InterruptedIOException();
                }
            }
        }

        @Override
        public synchronized void mark(int readlimit) {
            buffer.getInputStream().mark(readlimit);
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            checkState();

            try {
                if (closedByRemote && available() == 0)
                    return -1;
                threadAccessRecorder.record();
                fillBuffer();
                final int read = buffer.getInputStream().read(b, off, len);
                readBytes += read;
                return read;
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } catch (IOException e) {
                if (closedByRemote) {
                    // closed while trying to read
                    return -1;
                }
                throw e;
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    if (!closedByRemote)
                        throw new InterruptedIOException();
                }
            }
        }

        @Override
        public synchronized void reset() throws IOException {
            checkState();
            buffer.getInputStream().reset();
        }

        @Override
        public long skip(long n) throws IOException {
            checkState();

            try {
                threadAccessRecorder.record();
                fillBuffer();
                if (closedByRemote && available() == 0)
                    return -1;
                return buffer.getInputStream().skip(n);
            } catch (InterruptedException e) {
                throw new InterruptedIOException();
            } finally {
                try {
                    threadAccessRecorder.release();
                } catch (InterruptedException e) {
                    throw new InterruptedIOException();
                }
            }

        }

        protected void addPacket(StreamPacket packet) {
            if (isClosed()) {
                try {
                    packet.reject();
                } catch (IOException e) {
                    log.error("Could not reject packet: ", e);
                }
                return;
            }
            remainingPackets.add(packet);
            fillBuffer();
        }

        public StreamSession getSession() {
            return StreamSession.this;
        }

        public int streamID() {
            return streamID;
        }

        /**
         * Fills {@link #buffer} with data from {@link #remainingPackets} when
         * space is left
         */
        protected synchronized void fillBuffer() {
            if (remainingPackets.isEmpty())
                return;

            StreamPacket nextPacket = remainingPackets.peek();
            if (nextPacket == null)
                return;
            int remainingBuffer = buffer.getSpaceLeft();

            if (nextPacket.getSize() <= remainingBuffer) {
                remainingPackets.poll();
                try {
                    buffer.getOutputStream().write(nextPacket.getData());
                } catch (IOException e) {
                    log.error("Unexpected IOE: ", e);
                } catch (StreamException e) {
                    // ignore, we will be disposed :(
                }
            }
        }

        /**
         * Check state of stream and throw {@link IOException} when it is closed
         * local. When related {@link StreamSessionOutputStream} is closed, this
         * stream is closed when no more data is available.
         * 
         * @throws IOException
         *             when this stream itself is closed
         * @see #close()
         */
        protected void checkState() throws IOException {
            if (closed)
                throw new IOException("This stream is closed.");
            if (closedByRemote && remainingPackets.isEmpty() && available() == 0) {
                closed = true;
                threadAccessRecorder.interrupt();
            }
        }

        public boolean isClosed() {
            return closed || closedByRemote;
        }

    }

}