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

Java tutorial

Introduction

Here is the source code for de.fu_berlin.inf.dpp.net.internal.StreamServiceManager.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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang.ObjectUtils;
import org.apache.log4j.Logger;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Packet;
import org.picocontainer.Startable;
import org.picocontainer.annotations.Inject;

import com.google.common.collect.ImmutableList;

import de.fu_berlin.inf.dpp.Saros;
import de.fu_berlin.inf.dpp.User;
import de.fu_berlin.inf.dpp.annotations.Component;
import de.fu_berlin.inf.dpp.exceptions.ConnectionException;
import de.fu_berlin.inf.dpp.exceptions.ReceiverGoneException;
import de.fu_berlin.inf.dpp.exceptions.RemoteCancellationException;
import de.fu_berlin.inf.dpp.exceptions.SarosCancellationException;
import de.fu_berlin.inf.dpp.exceptions.StreamException;
import de.fu_berlin.inf.dpp.exceptions.StreamServiceNotValidException;
import de.fu_berlin.inf.dpp.net.IncomingTransferObject;
import de.fu_berlin.inf.dpp.net.IncomingTransferObject.IncomingTransferObjectExtensionProvider;
import de.fu_berlin.inf.dpp.net.JID;
import de.fu_berlin.inf.dpp.net.internal.StreamServiceManager.StreamMetaPacketData.StreamClose;
import de.fu_berlin.inf.dpp.net.internal.StreamSession.Stream;
import de.fu_berlin.inf.dpp.net.internal.StreamSession.StreamSessionListener;
import de.fu_berlin.inf.dpp.net.internal.StreamSession.StreamSessionOutputStream;
import de.fu_berlin.inf.dpp.net.internal.TransferDescription.FileTransferType;
import de.fu_berlin.inf.dpp.observables.SarosSessionObservable;
import de.fu_berlin.inf.dpp.observables.SessionIDObservable;
import de.fu_berlin.inf.dpp.project.AbstractSarosSessionListener;
import de.fu_berlin.inf.dpp.project.ISarosSession;
import de.fu_berlin.inf.dpp.project.ISharedProjectListener;
import de.fu_berlin.inf.dpp.project.SarosSessionManager;
import de.fu_berlin.inf.dpp.util.NamedThreadFactory;
import de.fu_berlin.inf.dpp.util.Utils;
import de.fu_berlin.inf.dpp.util.ValueChangeListener;

/**
 * <p>
 * This {@link StreamServiceManager} allows the registration of defined
 * {@link StreamService}'s which use {@link InputStream} or {@link OutputStream}
 * instead packets to communicate. It handles session-negotiation, sending and
 * receiving data through {@link StreamSession} streams and stopping sessions.
 * </p>
 * <p>
 * Short introduction to a {@link StreamSession}'s life-cycle:
 * <ol>
 * <li>Subclass {@link StreamService} to define your own service</li>
 * <li>Add it to our picoContainer</li>
 * <li>Start a session with
 * {@link #createSession(StreamService, User, Serializable, StreamSessionListener)}
 * </li>
 * <li>You receive a valid session or an exception when it was not possible to
 * establish one</li>
 * <li>Send ({@link StreamSession#getOutputStream(int)}) or receive (
 * {@link StreamSession#getInputStream(int)}) data through session's streams</li>
 * <li>Stop session with {@link StreamSession#stopSession()}</li>
 * </ol>
 * </p>
 * 
 * @author s-lau
 */
@Component(module = "net")
public class StreamServiceManager implements Startable {

    public static Logger log = Logger.getLogger(StreamServiceManager.class);

    /**
     * Timeout in seconds for canceling negotiations
     */
    public static final int SESSION_NEGOTIATION_TIMEOUT = 60;

    protected DataTransferManager dataTransferManager;

    protected Saros saros;

    protected SarosSessionManager sessionManager;

    @Inject
    protected SessionIDObservable sarosSessionID;

    protected SarosSessionObservable sarosSessionObservable;

    protected IncomingTransferObjectExtensionProvider incomingTransferObjectExtensionProvider;

    protected volatile boolean started = false;

    /**
     * Registered services with their name as key.
     */
    protected Map<String, StreamService> registeredServices = new HashMap<String, StreamService>();

    /**
     * All established sessions.
     */
    protected Map<StreamPath, StreamSession> sessions = Collections
            .synchronizedMap(new HashMap<StreamPath, StreamSession>());

    /**
     * Running initiations which wait for {@link StreamMetaPacketData#ACCEPT} or
     * {@link StreamMetaPacketData#REJECT} from receiver.
     */
    protected Map<StreamPath, Initiation> initiations = Collections
            .synchronizedMap(new HashMap<StreamPath, Initiation>());

    protected volatile PacketSender sender;

    protected volatile PacketReceiver receiver;

    /**
     * SessionID for next session
     */
    private AtomicInteger nextStreamSessionID = new AtomicInteger(1);

    /**
     * The time in seconds a session can shut down before it is killed.
     */
    public static final int SESSION_SHUTDOWN_LIMIT = 30;

    /**
     * Executor for stopping sessions after {@link #SESSION_SHUTDOWN_LIMIT}
     */
    protected ScheduledExecutorService stopSessionExecutor;

    /**
     * Dispatches sessions to their services
     */
    protected ExecutorService sessionDispatcher;

    /**
     * Handles negotiation of session to the end-user
     */
    protected ExecutorService negotiatesToUser;

    /**
     * Handles negotiation via network to receiver of session
     */
    protected ExecutorService negotiations;

    /**
     * Counts the incoming packets. Only useful for tracing.
     */
    private long counter = 0;

    public StreamServiceManager(XMPPReceiver xmppReceiver, DataTransferManager dataTransferManager,
            SarosSessionObservable sarosSessionObservable, Saros saros, SarosSessionManager sessionManager,
            List<StreamService> streamServices,
            IncomingTransferObjectExtensionProvider incomingTransferObjectExtensionProvider) {

        this.dataTransferManager = dataTransferManager;
        this.sarosSessionObservable = sarosSessionObservable;
        this.saros = saros;
        this.sessionManager = sessionManager;
        this.incomingTransferObjectExtensionProvider = incomingTransferObjectExtensionProvider;

        // add all valid services
        StringBuilder addedServicesNames = new StringBuilder();
        addedServicesNames.append("StreamServices added:");
        for (StreamService streamService : streamServices) {
            try {
                streamService.validate();
            } catch (StreamServiceNotValidException e) {
                log.error(
                        "StreamService '" + e.invalidService + "' is not valid, it will not be added.\nError is : ",
                        e);
                continue;
            }

            if (!addService(streamService)) {
                log.warn("Service '" + streamService + "' already added!");
            } else {
                addedServicesNames.append("\n  ");
                addedServicesNames.append(streamService.getServiceName());
            }
        }
        log.debug(addedServicesNames.toString());

        registerListeners();

        xmppReceiver.addPacketListener(new StreamPacketListener(), new StreamPacketFilter());
    }

    protected void startThreads() {
        sender = new PacketSender();
        Utils.runSafeAsync("StreamServiceManagers-senderThread", log, sender);
        receiver = new PacketReceiver();
        Utils.runSafeAsync("StreamServiceManagers-receiverThread", log, receiver);
        stopSessionExecutor = Executors.newScheduledThreadPool(5, new NamedThreadFactory("StreamSessionStopper-"));

        sessionDispatcher = Executors.newSingleThreadExecutor(new NamedThreadFactory("StreamSessionDispatcher-"));

        negotiatesToUser = Executors
                .newSingleThreadExecutor(new NamedThreadFactory("StreamSessionNegotiationUser-"));

        negotiations = Executors.newFixedThreadPool(5, new NamedThreadFactory("StreamSessionNegotiation-"));
    }

    public synchronized void start() {
        if (started)
            return;
        started = true;

        startThreads();
        counter = 0;
    }

    public synchronized void stop() {
        if (!started)
            return;
        started = false;

        stopSessionExecutor.shutdown();
        sessionDispatcher.shutdown();
        negotiatesToUser.shutdown();
        negotiations.shutdown();

        if (sender != null) {
            sender.dispose();
            sender = null;
        }
        if (receiver != null) {
            receiver.dispose();
            receiver = null;
        }

        synchronized (sessions) {
            // avoid ConcurrentModificationException when a sessions removes
            // itself
            for (StreamSession session : ImmutableList.copyOf(sessions.values())) {
                session.dispose();
            }
            sessions.clear();
        }

        for (Initiation initiation : initiations.values()) {
            initiation.cancel();
        }
        initiations.clear();

        stopSessionExecutor.shutdownNow();
        sessionDispatcher.shutdownNow();
        negotiatesToUser.shutdownNow();
        negotiations.shutdownNow();

        stopSessionExecutor = null;
        sessionDispatcher = null;
        negotiatesToUser = null;
        negotiations = null;
    }

    /**
     * Register {@link SharedProjectListener}, {@link ConnectionListener},
     * {@link SessionListener}.
     */
    protected void registerListeners() {
        final ISharedProjectListener sharedProjectListener = new SharedProjectListener();
        // re-add the listener when the session changes
        sarosSessionObservable.addAndNotify(new ValueChangeListener<ISarosSession>() {

            public void setValue(ISarosSession newValue) {
                if (newValue != null)
                    newValue.addListener(sharedProjectListener);
            }

        });
        sessionManager.addSarosSessionListener(new SessionListener());
    }

    /**
     * Adds a service if it has not been added yet. Session-passing etc. will
     * take place in that passed instance.
     * 
     * @param streamService
     *            to add
     * @return <code>true</code> when service was added
     */
    protected boolean addService(StreamService streamService) {
        return registeredServices.put(streamService.getServiceName(), streamService) == null;
    }

    /**
     * Notifies data arrived on a stream.
     * 
     * @param out
     *            stream where data arrived
     * @param forceSend
     *            force sending no matter how many bytes (>0) are written
     * @param progress
     */
    protected void notifyDataAvailable(StreamSessionOutputStream out, boolean forceSend,
            final SubMonitor progress) {

        if (sender != null)
            sender.addNotification(sender.new DataNotification(out, progress, forceSend));
    }

    /**
     * Closes the given stream and marks at the sink/source at receiver that
     * other side is closed.
     * 
     * @param stream
     */
    protected void closeStream(Stream stream) {
        if (stream.isClosed())
            return;

        StreamSession session = stream.getSession();
        StreamClose closeDescription = new StreamClose(stream instanceof InputStream, stream.streamID());

        if (sender != null)
            sender.sendPacket(session.getTransferDescription(),
                    StreamMetaPacketData.CLOSE.serializeInto(closeDescription), null);

    }

    /**
     * Given session will be terminated. A {@link StreamMetaPacketData#STOP} is
     * send to the receiver requesting him to stop. This method will notify
     * {@link StreamSessionListener#sessionStopped()} after packet was send.
     * 
     * @param session
     *            to terminate
     */
    protected void stopSession(final StreamSession session) {
        log.debug("stopping session " + session);

        if (session.shutdown != null) {
            log.warn("Session " + session + " is already being closed.");
            return;
        }

        // send stop packet
        TransferDescription transferDescription = session.getTransferDescription();

        if (sender != null)
            sender.sendPacket(transferDescription, StreamMetaPacketData.STOP.getIdentifier(), null);

        Runnable stopThread = Utils.wrapSafe(log, new SessionKiller(session));

        if (stopSessionExecutor != null) {
            stopSessionExecutor.schedule(stopThread, SESSION_SHUTDOWN_LIMIT, TimeUnit.SECONDS);
            session.shutdown = stopThread;
        } else {
            new Thread(stopThread).start();
        }

        if (session.sessionListener != null)
            session.sessionListener.sessionStopped();
    }

    /**
     * Given session finished to shut down after it was requested to stop
     * before. Signal it to other party with {@link StreamMetaPacketData#END}.
     * 
     * @param session
     *            which finished shutting down
     */
    protected void stoppedSession(StreamSession session) {
        if (session.stopped) {
            log.warn("Session " + session + " already stopped.");
            return;
        }
        log.info("Session " + session + " finished shutdown.");

        if (sender != null)
            sender.sendPacket(session.getTransferDescription(), StreamMetaPacketData.END.getIdentifier(), null);
    }

    /**
     * Convenient-method, create session with the default timeout
     * 
     * @threadsafe
     * @blocking
     * 
     * @param service
     *            service used for the session to be created
     * @param user
     *            start a session with
     * @param initiationDescription
     *            is passed to
     *            {@link StreamService#sessionRequest(User, Object)} at
     *            receiver's side, which is an optional description for the
     *            kind/purpose of this session. Can be <code>null</code>.
     * @param sessionListener
     *            Will be added to created session. Can be <code>null</code>.
     * @return A started session
     * @throws IllegalArgumentException
     *             when given {@link StreamService} is not found.
     * @throws TimeoutException
     *             Timeout reached, negotiation canceled.
     * @throws RemoteCancellationException
     *             Receiver rejected initiation-request.
     * @throws ExecutionException
     *             Unknown error happened during negotiation.
     * @throws InterruptedException
     *             Interrupted while negotiating.
     * @throws ConnectionException
     *             Not connected and can't send any data.
     * 
     * @see #SESSION_NEGOTIATION_TIMEOUT
     * @see #createSession(StreamService, User, Serializable,
     *      StreamSessionListener, int)
     */
    public StreamSession createSession(StreamService service, User user, Serializable initiationDescription,
            StreamSessionListener sessionListener) throws TimeoutException, RemoteCancellationException,
            ExecutionException, InterruptedException, ConnectionException {
        return createSession(service, user, initiationDescription, sessionListener, SESSION_NEGOTIATION_TIMEOUT);
    }

    /**
     * Try to establish a new session.
     * 
     * @threadsafe
     * @blocking
     * 
     * @param service
     *            service used for the session to be created
     * @param user
     *            start a session with
     * @param initiationDescription
     *            is passed to
     *            {@link StreamService#sessionRequest(User, Object)} at
     *            receiver's side, which is an optional description for the
     *            kind/purpose of this session. Can be <code>null</code>.
     * @param sessionListener
     *            Will be added to created session. Can be <code>null</code>.
     * @param timeout
     *            Cancel negotiation after given seconds.
     * @return A started session
     * @throws IllegalArgumentException
     *             when given {@link StreamService} is not found.
     * @throws TimeoutException
     *             Timeout reached, negotiation canceled.
     * @throws RemoteCancellationException
     *             Receiver rejected initiation-request.
     * @throws ExecutionException
     *             Unknown error happened during negotiation.
     * @throws InterruptedException
     *             Interrupted while negotiating.
     * @throws ConnectionException
     *             Not connected and can't send any data.
     */
    public StreamSession createSession(StreamService service, User user, Serializable initiationDescription,
            StreamSessionListener sessionListener, int timeout) throws TimeoutException,
            RemoteCancellationException, ExecutionException, InterruptedException, ConnectionException {
        if (!registeredServices.containsKey(service.getServiceName()))
            throw new IllegalArgumentException("Tried to create a stream with unregistered service " + service);
        int initiationID = nextStreamSessionID.getAndIncrement();

        StreamPath streamPath = new StreamPath(saros.getMyJID(), service, initiationID);

        if (initiationDescription != null) {
            byte[] serializedInitial = Utils.serialize(initiationDescription);
            if (serializedInitial == null)
                log.warn("Given serializable is not serializable! " + initiationDescription);
        }

        // holder for result
        Initiation initiation = new Initiation(service, initiationID, initiationDescription, streamPath,
                sessionListener);

        // store initiation
        initiations.put(streamPath, initiation);
        // send negotiation
        TransferDescription transferDescription = TransferDescription.createStreamMetaTransferDescription(
                user.getJID(), saros.getMyJID(), streamPath.toString(), sarosSessionID.getValue());

        if (sender != null)
            sender.sendPacket(transferDescription, StreamMetaPacketData.INIT.serializeInto(initiationDescription),
                    SubMonitor.convert(new NullProgressMonitor()));
        else
            throw new ConnectionException();

        Future<StreamSession> initiationProcess = negotiations.submit(initiation);

        StreamSession session;
        try {
            session = initiationProcess.get(timeout, TimeUnit.SECONDS);
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RemoteCancellationException) {
                RemoteCancellationException remoteCancellationException = (RemoteCancellationException) cause;
                throw remoteCancellationException;
            }
            log.error("Unknown error during negotiation: ", e.getCause());
            throw e;
        } finally {
            initiations.remove(streamPath);
        }

        return session;
    }

    /**
     * <p>
     * Represents a path to address packets to a particular
     * {@link StreamSession}.
     * </p>
     * <p>
     * The common representation is a {@link String} (used in
     * {@link TransferDescription#file_project_path}), which starts with the
     * type of packet (specified in {@link TransferDescription#type}) followed
     * by {@link #jid} (who initiated the session) and {@link #sessionID}.
     * </p>
     * <p>
     * Currently two types are used and implemented:
     * <ul>
     * <li> {@link TransferDescription.FileTransferType#STREAM_DATA}: A data
     * packet. Path additionally contains {@link #streamID} and {@link #size}.<br/>
     * Example:
     * 
     * <pre>
     * STREAM_DATA/alice1_fu@jabber.ccc.de/1/0/1048576
     * </pre>
     * 
     * </li>
     * <li> {@link TransferDescription.FileTransferType#STREAM_META}: A meta
     * packet. Path also stores the involved service name (to discover the
     * appropriate {@link StreamService} for an incoming initiation).
     * 
     * <pre>
     * STREAM_META/alice1_fu@jabber.ccc.de/1/SendFileSingle
     * </pre>
     * 
     * </li>
     * </ul>
     * All attributes are separated by {@link #PATH_DELIMITER}.
     * </p>
     * 
     */
    static class StreamPath {

        public static final char PATH_DELIMITER = '/';

        public String serviceName = null;
        public int size = 0;
        public int sessionID = 0;
        public int streamID = 0;
        /**
         * Is always the base JID, initiator of session
         */
        public String jid;
        public String type;

        /**
         * @throws IllegalArgumentException
         *             Given String can not be recognized as {@link StreamPath}
         */
        public StreamPath(String path) throws IllegalArgumentException {
            if (path == null)
                throw new IllegalArgumentException("Path can not be null");

            String[] tokens = path.split(String.valueOf(PATH_DELIMITER));

            this.type = tokens[0];
            if (this.type == null)
                throw new IllegalArgumentException("Type not known!");
            if (FileTransferType.STREAM_META.equals(this.type)) {
                if (tokens.length != 4)
                    throw new IllegalArgumentException("Unexpected number of tokens for a meta-path.");
                this.jid = tokens[1];
                this.serviceName = tokens[3];
                this.sessionID = Integer.valueOf(tokens[2]);
            } else if (FileTransferType.STREAM_DATA.equals(this.type)) {
                if (tokens.length != 5)
                    throw new IllegalArgumentException("Unexpected number of tokens for a data-path.");
                this.jid = tokens[1];
                this.sessionID = Integer.valueOf(tokens[2]);
                this.streamID = Integer.valueOf(tokens[3]);
                this.size = Integer.valueOf(tokens[4]);
            } else {
                throw new IllegalArgumentException("Type not valid!");
            }

        }

        /**
         * Builds a path for data packet.
         * 
         * @param jid
         *            initiator of session
         * @param sessionID
         *            of {@link StreamSession}
         * @param streamID
         *            to which stream in {@link StreamSession} this packet
         *            belongs
         * @param size
         *            of data in bytes
         */
        public StreamPath(JID jid, int sessionID, int streamID, int size) {
            this.type = FileTransferType.STREAM_DATA;
            this.jid = jid.getBase();
            this.size = size;
            this.sessionID = sessionID;
            this.streamID = streamID;
        }

        /**
         * Builds a path for meta packet.
         * 
         * @param jid
         *            initiator of session
         * @param service
         *            on which session is based
         * @param sessionID
         *            of {@link StreamSession} or it's initiation
         */
        public StreamPath(JID jid, StreamService service, int sessionID) {
            this.type = FileTransferType.STREAM_META;
            this.jid = jid.getBase();
            this.serviceName = service.getServiceName();
            this.sessionID = sessionID;

        }

        public JID getInitiator() {
            return new JID(jid);
        }

        /**
         * @return representation for this path as {@link String}
         */
        @Override
        public String toString() {
            if (FileTransferType.STREAM_DATA.equals(type)) {
                return String.format("%6$s%5$c%1$s%5$c%2$d%5$c%3$d%5$c%4$d", jid, sessionID, streamID, size,
                        PATH_DELIMITER, type);
            } else if (FileTransferType.STREAM_META.equals(type)) {
                return String.format("%5$s%4$c%1$s%4$c%3$d%4$c%2$s", jid, serviceName, sessionID, PATH_DELIMITER,
                        type);
            } else {
                throw new RuntimeException("Unknown type!");
            }
        }

        /**
         * hashCode is based on {@link #jid} and {@link #sessionID}
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((jid == null) ? 0 : jid.hashCode());
            result = prime * result + sessionID;
            return result;
        }

        /**
         * Two StreamPath's are equal when they represent the same session, when
         * {@link #sessionID} and {@link #jid} are equal.
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            StreamPath other = (StreamPath) obj;
            if (jid == null) {
                if (other.jid != null)
                    return false;
            } else if (!jid.equals(other.jid))
                return false;
            if (sessionID != other.sessionID)
                return false;
            return true;
        }

        protected static String createDataPath(StreamSession session, int streamID, int size) {
            return new StreamPath(session.initiator, session.sessionID, streamID, size).toString();
        }

        protected static String createMetaPath(StreamSession session) {
            return new StreamPath(session.initiator, session.getService(), session.sessionID).toString();
        }
    }

    /**
     * <p>
     * MetaPackets are session-life-cycle related packets of type
     * {@link FileTransferType#STREAM_META}. These {@link Enum}s contain the
     * data to identify the type of {@link StreamMetaPacketData}. Additionally
     * {@link Serializable}'s can be merged into data.
     * </p>
     * <p>
     * The format of a data-packet is one byte identifying the type (see
     * Enum-constants). When this packet should contain serialized data, it is
     * followed by a delimiter (null-byte) and the serialized object.
     * </p>
     * 
     * TODO serialize data with protobuf or xstream
     */
    static enum StreamMetaPacketData {
        /**
         * Initiate/request a session: expected to receive answer
         * {@link StreamMetaPacketData#ACCEPT} or
         * {@link StreamMetaPacketData#REJECT}. Can carry an initiation-object.
         */
        INIT(0x01),
        /**
         * Reject a session-request. Session will be closed.
         */
        REJECT(0x02),
        /**
         * Accepts a session-request. Session will be valid.
         */
        ACCEPT(0x03),
        /**
         * Request to stop a session. Expected to receive
         * {@link StreamMetaPacketData#END} when stopped.
         */
        STOP(0x04),
        /**
         * Confirm that session is ready to be killed.
         */
        END(0x05),
        /**
         * Closes a stream in a session. Carries {@link StreamClose} to describe
         * which stream is closed.
         */
        CLOSE(0x06);

        /**
         * Byte representing type of this packet.
         */
        byte identifier;

        protected static Map<Byte, StreamMetaPacketData> packets = new HashMap<Byte, StreamMetaPacketData>();

        static {
            for (StreamMetaPacketData m : StreamMetaPacketData.values())
                packets.put(m.identifier, m);
        }

        /**
         * 
         * @param identifier
         *            uses only lowest 8 bits as identifier
         */
        StreamMetaPacketData(int identifier) {
            this.identifier = (byte) identifier;
        }

        protected byte[] getIdentifier() {
            return new byte[] { this.identifier };
        }

        /**
         * Extracts an object from a {@link StreamMetaPacketData}'s data.
         * 
         * @param data
         * @return deserialized object or <code>null</code>
         */
        protected static Object deserializeFrom(byte[] data) {
            // at least one additional byte (which can be serialized data)
            if (data.length < 3)
                return null;
            // no MetaPacket (no identifier or delimiter)
            if (getPacket(data) == null || data[1] != 0)
                return null;

            byte[] serialized = new byte[data.length - 2];
            System.arraycopy(data, 2, serialized, 0, serialized.length);

            return Utils.deserialize(serialized);
        }

        /**
         * Merges a serialized {@link Object} into {@link StreamMetaPacketData}
         * 's data.
         * 
         * @param o
         * @return
         */
        protected byte[] serializeInto(Serializable o) {
            byte[] serialized = Utils.serialize(o);
            if (serialized == null)
                return new byte[] { this.identifier };

            byte[] result = new byte[serialized.length + 2];

            result[0] = this.identifier;
            result[1] = 0;
            System.arraycopy(serialized, 0, result, 2, serialized.length);

            return result;
        }

        protected static StreamMetaPacketData getPacket(byte[] data) {
            return data.length > 0 ? packets.get(data[0]) : null;
        }

        /**
         * Description for a stream which should be closed. After a local stream
         * has been closed, this is send to receiver that he can close his
         * stream.
         */
        static class StreamClose implements Serializable {
            private static final long serialVersionUID = -5326543581762056077L;
            /**
             * Was this stream an {@link InputStream} (at sender of this
             * object)? Then we should close the local output.
             */
            boolean senderInputstreamClosed;
            int streamID;

            public StreamClose(boolean senderInputstreamClosed, int streamID) {
                super();
                this.senderInputstreamClosed = senderInputstreamClosed;
                this.streamID = streamID;
            }

        }
    }

    /**
     * Holds the initiation (we send {@link StreamMetaPacketData#INIT}, wait for
     * response) of a session.
     */
    static class Initiation implements Callable<StreamSession> {

        protected StreamPath streamPath;
        protected StreamService service;
        protected StreamSessionListener sessionListener;
        protected int initiationID;
        protected Thread initiationThread = null;
        protected StreamSession session = null;
        /**
         * Blocks the call of {@link #call()} until attempt to negotiate a
         * session is rejected or accepted.
         * 
         * @see #startSession(StreamSession)
         * @see #rejectSession()
         */
        protected CountDownLatch responseLock = new CountDownLatch(1);
        protected Boolean rejected = null;
        protected Object initial;
        protected boolean cancelled = false;

        public Initiation(StreamService service, int initiationID, Object initial, StreamPath streamPath,
                StreamSessionListener sessionListener) {
            this.service = service;
            this.initiationID = initiationID;
            this.initial = initial;
            this.streamPath = streamPath;
            this.sessionListener = sessionListener;
        }

        /**
         * Blocks until {@link #rejectSession()} or
         * {@link #startSession(StreamSession)} are called.
         * 
         * @throws RemoteCancellationException
         *             Receiver rejected session
         * @throws InterruptedException
         *             Interrupted while waiting for result. This means
         *             {@link StreamServiceManager} is shutting down.
         */
        public StreamSession call() throws Exception {
            if (cancelled)
                throw new InterruptedException("Initiation was cancelled");
            initiationThread = Thread.currentThread();
            responseLock.await();
            assert rejected != null;
            if (rejected)
                throw new RemoteCancellationException("Recipient rejected session for " + service);
            assert session != null;
            session.setListener(sessionListener);
            return session;
        }

        /**
         * Let {@link #call()} return given {@link StreamSession}
         * 
         * @param session
         */
        protected synchronized void startSession(StreamSession session) {
            this.session = session;
            notifyWaiting(false);
        }

        /**
         * Let {@link #call()} throw a {@link SarosCancellationException}
         */
        protected synchronized void rejectSession() {
            notifyWaiting(true);

        }

        /**
         * Sets the rejected-status and releases lock in {@link #call()}
         * 
         * @param reject
         */
        private void notifyWaiting(boolean reject) {
            rejected = reject;
            responseLock.countDown();
        }

        /**
         * Cancels this initiation by interrupting this Thread if already
         * started.
         */
        public void cancel() {
            if (initiationThread != null)
                initiationThread.interrupt();
            cancelled = true;
        }

    }

    /**
     * <p>
     * Send data sequentially to {@link DataTransferManager}. A session's
     * {@link StreamSessionOutputStream} notifies when data was written, then
     * this running {@link Thread} will poll the stream for it's data after
     * processing earlier notifications.
     * </p>
     * <p>
     * Another purpose is sending data immediately via
     * {@link PacketSender#sendPacket(TransferDescription, byte[], SubMonitor)}.
     * </p>
     * <p>
     * Interrupting the Thread will cause a shutdown.
     * </p>
     * 
     * TODO handle SubMonitors that session's could use them
     */
    class PacketSender implements Runnable {

        protected BlockingQueue<DataNotification> notifications = new LinkedBlockingQueue<DataNotification>();

        /**
         * For these sessions no data should be send anymore
         */
        protected Set<StreamSession> blockedSessions = new HashSet<StreamSession>();

        protected Thread senderThread;

        protected volatile boolean disposed = false;

        /**
         * Packet which is send now
         */
        protected StreamPacket currentPacket = null;

        public void run() {
            senderThread = Thread.currentThread();
            while (true) {
                if (Thread.interrupted())
                    return;
                DataNotification notification = null;
                StreamPacket packetToSend = null;
                try {
                    notification = notifications.take();

                    synchronized (notifications) {
                        // only process notification when removeData(...) is not
                        // running
                        packetToSend = notification.getPacket();

                        if (packetToSend == null || blockedSessions.contains(packetToSend.getSession())) {
                            continue;
                        }
                    }
                    internalSend(packetToSend);
                } catch (InterruptedException e) {
                    // shutdown
                    return;
                } finally {
                    /*
                     * TODO .done() wrong when skipped notification: stream
                     * contained less than minimal-chunk-size and force send is
                     * false -> cache monitor until data is send
                     */

                    if (packetToSend != null && notification != null && notification.progress != null)
                        notification.progress.done();
                }

            }
        }

        /**
         * sends immediately to dtm
         */
        private synchronized void internalSend(StreamPacket packet) {
            try {
                currentPacket = packet;
                dataTransferManager.sendData(packet.getTransferDescription(), packet.data, packet.progress);

            } catch (IOException e) {
                log.error("Connection broken: ", e);
                if (packet.getSession() != null) {
                    packet.getSession().reportErrorAndDispose(new ConnectionException(e));
                    removeData(packet.getSession());
                }
            } catch (SarosCancellationException e) {
                /*
                 * ignore: user gone (will be reported by SharedProjectListener)
                 * or stream closed (drop data silently)
                 */
            }
        }

        /**
         * Sends immediately a packet to {@link DataTransferManager} when it's
         * not related to a established session. Otherwise it will be queued.
         * 
         * @param packet
         */
        protected void sendPacket(StreamPacket packet) {
            if (packet.getSession() == null)
                internalSend(packet);
            else
                notifications.add(new DataNotification(packet));

        }

        /**
         * Convenience method
         * 
         * @see #sendPacket(StreamPacket)
         */
        protected void sendPacket(TransferDescription transferDescription, byte[] data, SubMonitor progress) {
            try {
                sendPacket(new StreamPacket(transferDescription, data, progress));
            } catch (IllegalArgumentException e) {
                // packet invalid
                return;
            }
        }

        /**
         * Adds a {@link DataNotification} which will be processed later.
         * 
         * @param notification
         */
        protected void addNotification(DataNotification notification) {
            synchronized (notifications) {
                notifications.add(notification);
            }
        }

        /**
         * Removes all notifications and data for given session from queue,
         * aborts sending when data is send now and prevents that future data is
         * send.
         * 
         * @param session
         *            for which no data should be send anymore
         */
        protected void removeData(StreamSession session) {
            assert session != null;
            if (blockedSessions.contains(session))
                return;

            synchronized (notifications) {
                blockedSessions.add(session);
                if (currentPacket != null && session.getStreamPath().equals(currentPacket.streamPath))
                    currentPacket.progress.setCanceled(true);
                for (DataNotification notification : notifications) {
                    StreamPath streamPath = notification.getStreamPath();
                    if (streamPath.equals(session.getStreamPath()))
                        notifications.remove(notification);
                }
            }
        }

        protected void dispose() {
            if (disposed)
                return;
            disposed = true;

            senderThread.interrupt();

            synchronized (notifications) {
                for (DataNotification dn : notifications) {
                    if (dn.progress != null)
                        dn.progress.setCanceled(true);
                }
                notifications.clear();
            }

        }

        /**
         * This type of notification has two purposes
         * <ol>
         * <li>Notify some data in a stream</li>
         * <li>Notify a packet to be send</li>
         * </ol>
         */
        protected class DataNotification {
            StreamSessionOutputStream stream;
            SubMonitor progress;
            boolean removeAllAvailableData = false;
            StreamPacket packet;

            public DataNotification(StreamSessionOutputStream stream, SubMonitor progress,
                    boolean removeAllAvailableData) {
                super();
                this.stream = stream;
                this.progress = progress;
                this.removeAllAvailableData = removeAllAvailableData;
            }

            public DataNotification(StreamPacket packet) {
                this.packet = packet;
            }

            protected StreamPath getStreamPath() {
                return packet == null ? stream.getSession().getStreamPath() : packet.getStreamPath();
            }

            /**
             * 
             * @return packet which should be send or <code>null</code> when
             *         nothing to send
             */
            protected StreamPacket getPacket() {
                if (stream != null) {
                    StreamSession session = stream.getSession();
                    if (session.disposed || session.receiverStopped || session.stopped)
                        return null;

                    byte[] data = stream.getData(removeAllAvailableData);

                    if (data == null || (progress != null && progress.isCanceled()))
                        return null;

                    TransferDescription transferDescription = TransferDescription
                            .createStreamDataTransferDescription(stream.getSession().remoteJID, saros.getMyJID(),
                                    sarosSessionID.getValue(), stream.getStreamPath(data.length).toString());

                    try {
                        return new StreamPacket(transferDescription, data, progress);
                    } catch (IllegalArgumentException e) {
                        // packet invalid
                        return null;
                    }
                } else {
                    assert packet != null;
                    return packet;
                }
            }
        }
    }

    /**
     * <p>
     * This class receives all {@link IncomingTransferObject}'s from
     * {@link DataTransferManager} and processes
     * {@link FileTransferType#STREAM_META} and
     * {@link FileTransferType#STREAM_DATA}.
     * </p>
     * <p>
     * Interrupting the Thread will cause a shutdown.
     * </p>
     */
    class PacketReceiver implements Runnable {

        protected BlockingQueue<StreamPacket> incomingPackets = new LinkedBlockingQueue<StreamPacket>();

        protected Thread receiverThread;

        protected volatile boolean disposed = false;

        public void run() {
            receiverThread = Thread.currentThread();
            while (true) {
                StreamPacket packet;
                try {
                    packet = incomingPackets.take();
                } catch (InterruptedException e) {
                    return;
                }

                processPacket(packet);

                if (Thread.interrupted()) {
                    return;
                }
            }
        }

        protected synchronized void dispose() {
            if (disposed)
                return;
            disposed = true;

            receiverThread.interrupt();
            for (StreamPacket p : incomingPackets) {
                try {
                    p.reject();
                } catch (IOException e1) {
                    break;
                }
            }
            incomingPackets.clear();
        }

        /**
         * Adds an incoming packet to our working queue to process it later.
         * 
         * @param packet
         */
        protected void offerPacket(StreamPacket packet) {
            if (disposed) {
                try {
                    packet.reject();
                } catch (IOException e) {
                    // ignore
                }
                return;
            }

            incomingPackets.add(packet);
        }

        /**
         * Process an incoming {@link StreamPacket}.
         * {@link FileTransferType#STREAM_DATA}-packets are passed to session,
         * {@link FileTransferType#STREAM_META}-packets will be processed by
         * {@link #processMeta(StreamPacket)}
         * 
         * @param packet
         */
        protected void processPacket(StreamPacket packet) {
            counter++;
            TransferDescription description = packet.getTransferDescription();

            log.trace("Packet " + counter + " arrived");
            if (FileTransferType.STREAM_DATA.equals(description.type)) {
                StreamSession session = sessions.get(packet.getStreamPath());
                if (session == null) {
                    log.error("Received packet for an unknown session. Path is " + packet.getStreamPath());
                    try {
                        packet.reject();
                    } catch (IOException e) {
                        log.warn("Could not reject unknown data packet: ", e);
                    }
                    return;
                }
                session.addPacket(packet);
            } else if (FileTransferType.STREAM_META.equals(description.type)) {
                processMeta(packet);
            } else {
                log.error("Received unknown packet type: " + description.type);
            }

        }

        /**
         * Process incoming {@link StreamMetaPacketData}'s
         */
        protected void processMeta(StreamPacket packet) {
            byte[] data;

            try {
                data = packet.getData();
            } catch (StreamException e) {
                log.warn("Could not open packet: ", e);
                // stop processing
                return;
            }

            final TransferDescription transferDescription = packet.getTransferDescription();
            // get type of packet
            StreamMetaPacketData metaPacket = StreamMetaPacketData.getPacket(data);
            if (metaPacket == null) {
                log.error("Received unknown meta packet: " + new String(data));
                return;
            }

            final Initiation initiation;
            final StreamPath streamPath;
            try {
                streamPath = new StreamPath(transferDescription.file_project_path);
            } catch (IllegalArgumentException e) {
                log.error(
                        "Packet had invalid stream-path: " + (transferDescription.file_project_path == null ? "none"
                                : transferDescription.file_project_path));
                return;
            }
            final StreamSession session = sessions.get(streamPath);

            log.debug("Received " + metaPacket.name() + " for session " + streamPath);
            switch (metaPacket) {
            case INIT:
                // remote peer wants to create a session

                // validate service
                final StreamService service = registeredServices.get(streamPath.serviceName);
                if (service == null) {
                    log.error("Received inititation request for unknown service: " + streamPath.serviceName);
                    return;
                }

                final Object initiationDescription = StreamMetaPacketData.deserializeFrom(data);

                if (sessions.containsKey(streamPath)) {
                    log.error("Received initiation packet more than once from " + transferDescription.sender);
                    return;
                }

                ISarosSession sarosSession = sarosSessionObservable.getValue();
                if (sarosSession == null) {
                    log.warn("Not in a shared project, discarding packet!");
                    return;
                }

                final User from = sarosSession.getUser(transferDescription.sender);
                if (from == null) {
                    log.warn("Buddy left, discarding packet!");
                    return;
                }

                /*
                 * Ask service for accept and send decision to client. When
                 * accepted, a new session will be created.
                 */
                negotiatesToUser.execute(Utils.wrapSafe(log, new Runnable() {
                    public void run() {
                        log.debug("Starting session request to service");

                        boolean startSession = false;
                        try {
                            startSession = service.sessionRequest(from, initiationDescription);
                        } catch (Exception e) {
                            log.error("Service crashed: ", e);
                        }

                        if (startSession) {
                            log.debug("Service accepted, try to create session");

                            final StreamSession newSession;
                            synchronized (sessions) {
                                if (sessions.containsKey(streamPath)) {
                                    log.warn("Session already created, received INIT twice: " + streamPath);
                                    return;
                                }
                                newSession = new StreamSession(StreamServiceManager.this, service,
                                        transferDescription.sender, transferDescription.sender,
                                        streamPath.sessionID, initiationDescription);

                            }

                            if (sender != null) {
                                sender.sendPacket(newSession.getTransferDescription(),
                                        StreamMetaPacketData.ACCEPT.getIdentifier(), null);
                                log.debug("Accept-packet send.");
                                sessions.put(streamPath, newSession);
                            } else
                                // can not create, not connected
                                newSession.dispose();

                            sessionDispatcher.execute(Utils.wrapSafe(log, new Runnable() {
                                public void run() {
                                    service.startSession(newSession);
                                    log.debug("Session started");
                                }
                            }));
                        } else {
                            log.debug("Session rejected, will send reject-packet.");

                            if (sender != null) {
                                sender.sendPacket(
                                        TransferDescription.createStreamMetaTransferDescription(
                                                transferDescription.sender, transferDescription.recipient,
                                                streamPath.toString(), sarosSessionID.getValue()),
                                        StreamMetaPacketData.REJECT.getIdentifier(), null);

                                log.debug("Reject-packet send.");
                            }
                        }

                    }
                }));

                break;
            case STOP:
                // remote peer wants to terminate this session

                if (session == null) {
                    log.error("Received STOP-packet for unknown session " + streamPath);
                    return;
                }
                log.debug("Received STOP for session " + session);

                if (session.shutdown != null) {
                    log.error("Session " + session + " already stopped.");
                    return;
                }
                Runnable stopThread = Utils.wrapSafe(log, new SessionKiller(session));
                session.shutdown = stopThread;

                stopSessionExecutor.schedule(stopThread, SESSION_SHUTDOWN_LIMIT, TimeUnit.SECONDS);

                if (session.sessionListener != null)
                    session.sessionListener.sessionStopped();

                break;
            case END:
                // remote peer finished shutdown and disposed his session

                if (session == null) {
                    log.warn("Received END for an unknown session:" + streamPath);
                    return;
                }

                if (session.receiverStopped) {
                    log.warn("Receiver stopped already, received another STOPPED " + session);
                    return;
                }

                session.receiverStopped = true;

                if (sender != null)
                    sender.removeData(session);

                break;
            case REJECT: //$FALL-THROUGH$
            case ACCEPT:
                initiation = initiations.remove(streamPath);

                if (initiation == null) {
                    log.warn("Received REJECT/ACCEPT packet I have no initiation for!");
                    return;
                }
                if (metaPacket == StreamMetaPacketData.REJECT)
                    initiation.rejectSession();
                else {
                    final StreamSession newSession = new StreamSession(StreamServiceManager.this,
                            initiation.service, transferDescription.sender, saros.getMyJID(), streamPath.sessionID,
                            initiation.initial);
                    sessions.put(streamPath, newSession);

                    initiation.startSession(newSession);
                    log.debug("Session started, passed to initiation.");
                }
                break;
            case CLOSE:
                // a stream in a session closed

                if (session == null) {
                    log.warn("Received CLOSE for a session which not exists");
                    return;
                }

                Object o = StreamMetaPacketData.deserializeFrom(data);
                StreamClose closeDesc;
                if (o instanceof StreamClose) {
                    closeDesc = (StreamClose) o;
                } else {
                    log.error("Received unknown object in CLOSE-packet");
                    return;
                }

                Stream toClose = (Stream) (closeDesc.senderInputstreamClosed
                        ? session.getOutputStream(closeDesc.streamID)
                        : session.getInputStream(closeDesc.streamID));

                toClose.closedByRemote();

                break;
            default:
                log.error("Please implement case for this unknown metapacket with identifier "
                        + Byte.valueOf(data[0]));

            }

        }
    }

    /**
     * This {@link Runnable} invalidates/disposes a session.
     */
    class SessionKiller implements Runnable {

        private StreamSession session;

        public SessionKiller(StreamSession session) {
            super();
            this.session = session;
        }

        public void run() {
            log.debug("Killing session " + session);
            StreamPath streamPath = session.getStreamPath();
            if (sessions.get(streamPath) == null) {
                log.warn("Session " + session + " already closed");
                return;
            }
            if (!(session.receiverStopped && session.stopped)) {
                String bad;
                if (session.receiverStopped || session.stopped)
                    bad = session.stopped ? "receiver" : "us";
                else
                    bad = "both";
                log.warn("Session not properly shut downed by " + bad + ". Will kill session " + session);
            }
            session.dispose();
            sessions.remove(streamPath);
        }
    }

    /**
     * Abstraction for incoming and outgoing data-packets.
     */
    public class StreamPacket {
        /**
         * Timeout for receiving data (
         * {@link IncomingTransferObject#accept(SubMonitor)}) in seconds.
         */
        public static final int DATA_RECEIVE_TIMEOUT = 10;

        protected TransferDescription transferDescription = null;
        protected IncomingTransferObject ito = null;
        protected byte[] data = null;
        protected StreamPath streamPath;
        protected SubMonitor progress = null;

        /**
         * Incoming packet.
         * 
         * @param ito
         * @throws IllegalArgumentException
         *             Transfer-object contains no valid {@link StreamPath}
         */
        public StreamPacket(IncomingTransferObject ito) throws IllegalArgumentException {
            this.ito = ito;
            this.transferDescription = ito.getTransferDescription();
            this.streamPath = new StreamPath(ito.getTransferDescription().file_project_path);
        }

        /**
         * Outgoing packet.
         * 
         * @param desc
         * @param data
         * @throws IllegalArgumentException
         *             When descriptions file-path is not a {@link StreamPath}
         */
        public StreamPacket(TransferDescription desc, byte[] data, SubMonitor progress)
                throws IllegalArgumentException {
            this.transferDescription = desc;
            this.data = data;
            this.progress = progress == null ? SubMonitor.convert(new NullProgressMonitor()) : progress;
            this.streamPath = new StreamPath(desc.file_project_path);
        }

        /**
         * Reject this packet (when incoming).
         * 
         * @throws IOException
         * @see IncomingTransferObject#reject()
         */
        public void reject() throws IOException {
            if (ito != null)
                ito.reject();
        }

        /**
         * Returns the data this packet contains. When this packet is incoming,
         * the first call blocks, then cached. When an error occurs and can not
         * receive any data, it's is reported to packets session, if there is
         * one, and {@link StreamException} will be thrown.
         * 
         * @blocking
         * @caching
         * @throws StreamException
         *             when any error occurs and no data can be retrieved
         */
        public byte[] getData(final SubMonitor progress) throws StreamException {
            if (this.data != null)
                return this.data;

            try {
                data = ito.accept(progress == null ? SubMonitor.convert(null) : progress);

            } catch (SarosCancellationException cancellation) {
                log.error("Receiver cancelled unexpected: ", cancellation);
                if (this.getSession() != null)
                    this.getSession().reportErrorAndDispose(new ReceiverGoneException(cancellation));
            } catch (IOException ioe) {
                log.error("Connection broken: ", ioe);
                if (this.getSession() != null)
                    this.getSession().reportErrorAndDispose(new ConnectionException(ioe));

            }

            if (data == null || data.length == 0) {
                // received none -> error
                throw new StreamException("Packet contained no data");
            }

            if (transferDescription.type.equals(FileTransferType.STREAM_DATA) && data.length != streamPath.size) {
                if (streamPath.size > data.length) {
                    log.error("Lost bytes! Got " + data.length + ", expected " + streamPath.size);
                    throw new StreamException("Received less data than announced.");
                }
                byte[] new_data = new byte[streamPath.size];
                System.arraycopy(data, 0, new_data, 0, streamPath.size);
                data = new_data;
            }
            return this.data;
        }

        /**
         * Returns the data this packet contains. When this packet is incoming,
         * the first call blocks, then cached. When an error occurs and can not
         * receive any data, it's is reported to packets session, if there is
         * one, and {@link StreamException} will be thrown.
         * 
         * @blocking
         * @caching
         * @throws StreamException
         *             when any error occurs and no data can be retrieved
         * 
         * @see #getData(SubMonitor)
         */
        public byte[] getData() throws StreamException {
            return getData(null);
        }

        /**
         * @return number of bytes in packets data
         */
        public int getSize() {
            return streamPath.size;
        }

        public StreamPath getStreamPath() {
            return this.streamPath;
        }

        /**
         * @return session for/of this packet
         */
        public StreamSession getSession() {
            return sessions.get(streamPath);
        }

        public TransferDescription getTransferDescription() {
            return transferDescription;
        }

        /**
         * @return <code>true</code> when we received this packet
         */
        public boolean isIncoming() {
            return ito != null;
        }

    }

    class StreamPacketListener implements PacketListener {

        public void processPacket(Packet packet) {
            IncomingTransferObject ito = incomingTransferObjectExtensionProvider.getPayload(packet);
            try {
                if (receiver != null)
                    receiver.offerPacket(new StreamPacket(ito));
            } catch (IllegalArgumentException e) {
                log.error("Received not valid packet: " + ito);
                return;
            }
        }
    }

    class StreamPacketFilter implements PacketFilter {

        public boolean accept(Packet packet) {
            IncomingTransferObject payload = incomingTransferObjectExtensionProvider.getPayload(packet);

            if (payload == null) {
                return false;
            }

            TransferDescription transferDescription = payload.getTransferDescription();
            if (!Utils.equals(transferDescription.sessionID, sarosSessionID.getValue()))
                return false;

            return ObjectUtils.equals(transferDescription.type, FileTransferType.STREAM_DATA)
                    || ObjectUtils.equals(transferDescription.type, FileTransferType.STREAM_META);
        }
    }

    /**
     * Removes a user's sessions when he leaves the shared project
     */
    protected final class SharedProjectListener implements ISharedProjectListener {
        public void userLeft(User user) {
            // remove his sessions
            synchronized (sessions) {
                // avoid ConcurrentModificationException when a sessions removes
                // itself
                for (StreamSession session : ImmutableList.copyOf(sessions.values())) {
                    if (session.remoteJID.equals(user.getJID())) {
                        session.reportErrorAndDispose(new ReceiverGoneException());
                    }
                }
            }
        }

        public void userJoined(User user) {
            // NOP
        }

        public void permissionChanged(User user) {
            // NOP
        }

        public void invitationCompleted(User user) {
            // NOP
        }

        public void projectAdded(IProject project) {
            // NOP

        }
    }

    /**
     * Resets the {@link StreamServiceManager} when saros' session stopped.
     */
    protected final class SessionListener extends AbstractSarosSessionListener {

        @Override
        public void sessionStarted(ISarosSession newSarosSession) {
            StreamServiceManager.this.start();
        }

        @Override
        public void sessionEnded(ISarosSession oldSarosSession) {
            StreamServiceManager.this.stop();
        }

    }

}