view java/src/uk/ac/qmul/eecs/ccmi/network/ServerConnectionManager.java @ 1:e3935c01cde2 tip

moved license of PdPersistenceManager to the beginning of the file
author Fiore Martin <f.martin@qmul.ac.uk>
date Tue, 08 Jul 2014 19:52:03 +0100
parents 78b7fc5391a2
children
line wrap: on
line source
/*  
 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
  
 Copyright (C) 2011  Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)

 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 3 of the License, 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, see <http://www.gnu.org/licenses/>.
*/  

package uk.ac.qmul.eecs.ccmi.network;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

import javax.swing.SwingUtilities;

import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramTreeNode;
import uk.ac.qmul.eecs.ccmi.gui.Diagram;
import uk.ac.qmul.eecs.ccmi.gui.DiagramEventSource;
import uk.ac.qmul.eecs.ccmi.gui.Edge;
import uk.ac.qmul.eecs.ccmi.gui.Finder;
import uk.ac.qmul.eecs.ccmi.gui.Lock;
import uk.ac.qmul.eecs.ccmi.gui.Node;
import uk.ac.qmul.eecs.ccmi.gui.awareness.AwarenessPanelEditor;
import uk.ac.qmul.eecs.ccmi.gui.awareness.BroadcastFilter;
import uk.ac.qmul.eecs.ccmi.gui.awareness.DisplayFilter;
import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager;
import uk.ac.qmul.eecs.ccmi.pdsupport.PdDiagram;
import uk.ac.qmul.eecs.ccmi.pdsupport.PdPersistenceManager;
import uk.ac.qmul.eecs.ccmi.speech.Narrator;
import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;

/* This class manages the different sessions with the clients. whereas the 
 * Server class just listens for connections, this class manages all what's going on about the
 * diagram editing: e.g. command processing consistency check, client updates etc.
 */
class ServerConnectionManager  {
	ServerConnectionManager( Map<String,Diagram> diagrams, AwarenessPanelEditor panelEditor) {
		this.diagrams = diagrams;
		awarenessPanelEditor = panelEditor;
		diagramChannelAllocations = new HashMap<Diagram,Set<UserNameSocketChannel>>();
		localhostDiagramElementQueue = new ConcurrentHashMap<String,Queue<DiagramElement>>();
		protocol = ProtocolFactory.newInstance();
		lockManager = new ServerLockManager();
		broadcastFilter = BroadcastFilter.getInstance();
	}
	
	/**
	 * Removes the channel and the locks related to it from the inner data structures.
	 * 
	 * @param channel the channel to remove
	 */
	void removeChannel(SocketChannel channel) throws IOException{
		String diagramName = null;
		/* looks for the Set containing this channel */
		for(Map.Entry<Diagram,Set<UserNameSocketChannel>> entry : diagramChannelAllocations.entrySet()){
			UserNameSocketChannel unsc = null;
			for(UserNameSocketChannel userNameSocketChannel : entry.getValue())
				if(userNameSocketChannel.channel.equals(channel)){
					unsc = userNameSocketChannel;
					break;
				}
			/* remove the channel from this set of channels */
			if(entry.getValue().remove(unsc) && !unsc.userName.isEmpty()){
				diagramName = entry.getKey().getName();
				awarenessPanelEditor.removeUserName(diagramName, unsc.userName);
				/* notify the other clients the user has disconnected */
				AwarenessMessage awMsg = new AwarenessMessage(
						AwarenessMessage.Name.USERNAME_A,
						diagramName,
						""+AwarenessMessage.USERNAMES_SEPARATOR+unsc.userName
						);
				for(UserNameSocketChannel userNameSocketChannel : entry.getValue()){
					/* don't send aw msg to the local channel */
					if(!userNameSocketChannel.channel.equals(localChannel))
						protocol.send(userNameSocketChannel.channel, awMsg);
				}
				break;
			}
			
		}
		/* all locks held by disconnected user are released */
		lockManager.removeLocks(channel, diagramName);
	}
	
	void handleMessage(SocketChannel channel) throws IOException{
		Message message = protocol.receiveMessage(channel);
		if(message instanceof Command){
			handleCommand((Command)message, channel);
		}else if(message instanceof LockMessage) {
			handleLockMessage((LockMessage)message,channel);
		}else { // awareness message - broadcast the message 
			handleAwarenessMessage((AwarenessMessage)message,channel,null);
		}
	}
	
	private void handleLockMessage(LockMessage lockMessage, SocketChannel channel) throws IOException{
		Lock lock = LockMessageConverter.getLockFromMessageName((LockMessage.Name)lockMessage.getName());
		String name = lockMessage.getName().toString();
		String diagramName = lockMessage.getDiagram(); 
		
		Diagram diagram = null;
		synchronized(diagrams){
			diagram = diagrams.get(diagramName);
		}
		if(diagram == null){
			replyLockMessage(channel,diagramName,false);
			return;
		}
		
		/* spot the tree node the message refers to */		
		int[] path = new int[lockMessage.getArgNum()];
		for(int i = 0; i< path.length;i++){
			path[i] = (Integer)lockMessage.getArgAt(i);
		}
		
		DiagramTreeNode treeNode = null;
		/* synchronize with the event dispatching thread */
		ReentrantLock monitor = diagram.getCollectionModel().getMonitor(); 
		monitor.lock();

		treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot());
		/* the tree node has been deleted, lock cannot be granted */
		if(treeNode == null){
			monitor.unlock();
			replyLockMessage(channel,diagramName,false);
			return;
		}
		//System.out.println("Lock message received: " + name +" diagram:"+diagramName+" treenode:"+treeNode.getName() );
		
		/* check whether it's a GET or YIELD message and act accordingly */
		if(name.startsWith(LockMessage.GET_LOCK_PREFIX)){
			// System.out.println("get lock source:"+ lockMessage.getSource());
			boolean succeeded;
			succeeded = lockManager.requestLock(treeNode, lock, channel, diagramName);
			monitor.unlock();
			/* send the response */
			replyLockMessage(channel,diagramName,succeeded);
			if(succeeded && broadcastFilter.accept(lockMessage.getSource())){
				DiagramEventActionSource processedSource = broadcastFilter.process(lockMessage.getSource()); // changes according to configuration; 
				
				//select node is a temporary record, therefore it doesn't need to be stored 
				if(processedSource.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION){
					Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram);
					/* saves the diagramEventActionSource for when the lock is yielded */
					if(userNames != null){
						for(UserNameSocketChannel userName : userNames){
							if(userName.channel.equals(channel)){
								userName.lockAwarenessSources.add(processedSource);
							}
						}
					}
				}
				/* handle the awareness message piggybacked in the lock message */
				AwarenessMessage awarMsg = new AwarenessMessage(
						AwarenessMessage.Name.START_A,
						diagramName,
						processedSource
						);
				handleAwarenessMessage(awarMsg,channel,diagram);
			}
		}else{ // yield lock 
			boolean released = lockManager.releaseLock(treeNode, lock, channel,diagramName);
			monitor.unlock();
			DiagramEventActionSource source = lockMessage.getSource(); 
			/* it's NULL for NOTES lock and SELECT_NODE_FOR_EDGE_CREATION must not clean the text panel, because its record is temporary */ 
			if(released && source != DiagramEventActionSource.NULL && source.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION){ 
				
				if(source.getCmd() == Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION && broadcastFilter.accept(source)){
					/* unselect node for edge creation is treated differently because it doesn't * 
					 * clear the text panel but adds another record, which is temporary          */
					handleAwarenessMessage(new AwarenessMessage(
							AwarenessMessage.Name.STOP_A,
							diagramName,
							source),
						channel,
						diagram);
					return;
				}
				
				/* retrieves the diagramEventActionSource: when the lock was gotten, the source was stored in   *
				 * userName.lockAwarenessSource. This is done because the broadcast filter configuration might  *
				 * have changed in the meanwhile but we still need to send the aw msg with the original source  *
				 * or the clients won't be able to pick out the string to delete                                */
				DiagramEventActionSource savedSource = removeEventActionSource(channel,source.getSaveID(),diagram);

				/* saved source = null means the broadcast filter didn't let the get_lock message 
				 * this yield_lock message is referring to. Move on */
				if(savedSource == null){
					return;
				}
				
				AwarenessMessage awMsg = new AwarenessMessage(
						AwarenessMessage.Name.STOP_A,
						diagramName,
						savedSource
						);
				handleAwarenessMessage(awMsg,channel,diagram);
			}
		}
	}
	
	private void handleCommand(final Command cmd, SocketChannel channel) throws IOException{	
		/* init some variables we're gonna use in (nearly) every branch of the switch */
		final String diagramName = cmd.getDiagram();
		Diagram diagram = null;
		
		if(cmd.getName() != Command.Name.LIST){
			synchronized(diagrams){
				diagram = diagrams.get(diagramName);
			}
			if(diagram == null)
				protocol.send(channel, new Reply(Reply.Name.ERROR_R,diagramName,"Diagram "+diagramName+" does not exists",cmd.getSource()));
		}
		Node node = null;
		Edge edge = null;
		boolean broadcast = true;
		
		DiagramEventSource source = cmd.getSource();
		if(channel == localChannel)
			source = source.getLocalSource();
		/* set the diagram id so the haptic will update the diagram specified by the command and not the active tab's */
		if(diagram != null)
			source.setDiagramName(diagram.getName());
		/* log the command through the interaction logger, if any */
		Command.log(cmd,(channel == localChannel) ? "local command received" : "remote command received");
		//System.out.println("ServerConnectionManager: received command "+cmd.getName()); 
		switch(cmd.getName()){
		case LOCAL : // the local socket makes itself known to the server
			localChannel = channel;
			Set<UserNameSocketChannel> list = new HashSet<UserNameSocketChannel>();
			list.add(new UserNameSocketChannel(localChannel,AwarenessMessage.getDefaultUserName()));
			diagramChannelAllocations.put(diagram, list);
			broadcast = false;
			break;
		case LIST : // ask for the list of available diagrams on the server
			StringBuilder names = new StringBuilder(""); 
			synchronized(diagrams){
				for(String s : diagrams.keySet()){
					names.append(s).append('\n');
				}
			}
			protocol.send(channel, new Reply(Reply.Name.LIST_R,"",names.toString(),DiagramEventSource.NONE));
			broadcast = false;
			break;
		case GET : // ask for a diagram xml
			try{
				diagram.getCollectionModel().getMonitor().lock();
				Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram); 
				ByteArrayOutputStream out = new ByteArrayOutputStream();
				if(diagram instanceof PdDiagram){
					PdPersistenceManager.getInstance().encodeDiagramInstance(diagram, new BufferedOutputStream(out));
				}else{
					PersistenceManager.encodeDiagramInstance(diagram, new BufferedOutputStream(out));
				}
				diagram.getCollectionModel().getMonitor().unlock();
				try{
					protocol.send(channel, new Reply(Reply.Name.GET_R,diagramName,out.toString("UTF-8"),DiagramEventSource.NONE));
					for(UserNameSocketChannel sc: userNames){
						protocol.send(channel, new AwarenessMessage(AwarenessMessage.Name.USERNAME_A,diagramName,sc.userName));
					}
				}catch(IOException ioe){
					throw ioe;
				}
				userNames.add(new UserNameSocketChannel(channel));
				broadcast = false;
			}catch(Exception e){
				// close the socket, log and discard the packet
				try{channel.close();}catch(IOException ioe){ioe.printStackTrace();}
				Server.logger.severe(e.getMessage());
			}
			break;
		case INSERT_NODE : 
			if(channel == localChannel){
				/* if the command is coming from the local user then there is           *
				 * a diagram element queued, which is the one the user wanted to insert */		 
				node = (Node)localhostDiagramElementQueue.get(diagramName).poll();
			}else{
				double dx = (Double)cmd.getArgAt(1);
				double dy = (Double)cmd.getArgAt(2);
				node = Finder.findNode((String)cmd.getArgAt(0),diagram.getNodePrototypes());
				node = (Node)node.clone();
				/* Place the top left corner of the bounds at the origin. It might be different from     *
        		 * the origin, as it depends on how the clonation is implemented internally. These calls * 
        		 * to translate are not notified to any listener as the node is not in the model yet     */
        		Rectangle2D bounds = node.getBounds();
        		node.translate(new Point2D.Double(),-bounds.getX(),-bounds.getY(),DiagramEventSource.NONE);
				/* perform the actual translation from the origin */
        		node.translate(new Point2D.Double(), dx, dy, DiagramEventSource.NONE);
			}
			/* wait for the node to be inserted in the model so that it gets an id, which is then   *
			 * used in the awareness message to notify other users that the node has been inserted  */
			try {
				SwingUtilities.invokeAndWait(new CommandExecutor.Insert(diagram.getCollectionModel(),node,null,source));
			} catch (Exception exception) {
				throw new RuntimeException(exception); // must never happen 
			}
			/* send the reply to the client which issued the command */
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.INSERT_NODE_R,diagramName,"Insert new Node",source.getLocalSource()));
			
			DiagramEventActionSource actionSource = new DiagramEventActionSource(source,
					Command.Name.INSERT_NODE,node.getId(),node.getName()); 
			if(broadcastFilter.accept(actionSource)){
				/* must set the username to the one of the client who sent the command       *
				 * otherwise the local username is automatically assigned in the constructor */
				for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
					if(sc.channel.equals(channel))
						actionSource.setUserName(sc.userName);
				}
				/* process on the broadcast filter */
				DiagramEventActionSource processedSource = broadcastFilter.process(actionSource);
				/* since no lock is used we must send an awareness message without piggybacking */
				AwarenessMessage awMsg = new AwarenessMessage(
						AwarenessMessage.Name.START_A,
						diagramName,
						processedSource
						); 
				if(channel != localChannel){
					/* update the local awareness panel and speech */ 
					awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), DisplayFilter.getInstance().processForText(processedSource));
					NarratorFactory.getInstance().speakWholeText(DisplayFilter.getInstance().processForSpeech(processedSource), Narrator.SECOND_VOICE);
				}
				/* broadcast the awareness message to all the clients but one which sent the    *
				 * command and the local one to inform them the action has started              */
				for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
					if(!sc.channel.equals(channel) && !sc.channel.equals(localChannel)){
						protocol.send(sc.channel, awMsg);
					}
				}
			}
			break;
		case REMOVE_NODE :
			if(channel == localChannel){
				/* if the command is coming from the local user then there is 		    *
				 * a diagram element queued, which is the one the user wants  to remove */
				node = (Node)localhostDiagramElementQueue.get(diagramName).poll();
				lockManager.removeLocks(node, diagramName);
			}else{
				diagram.getCollectionModel().getMonitor().lock();
				node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
				lockManager.removeLocks(node, diagramName);
				diagram.getCollectionModel().getMonitor().unlock();
			}
			/* remove the action source, like when a lock is yielded, for this node */
			removeEventActionSource(channel,node.getId(),diagram);
			/* wait for the Event Dispatching Thread to delete the edge, in order to avoid collisions  *
			 * with other locks, e.g. locking again an edge before the EDT deletes it                  */
			try {
				SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(),node,source));
			} catch (Exception exception) {
				throw new RuntimeException(exception); // must never happen 
			}
			/* send the reply to the client which issued the command */
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.REMOVE_NODE_R,diagramName,"Node with id "+ node.getId() +" removed",source.getLocalSource()));
			break;
		case INSERT_EDGE :
			long[] nodesToConnect = null;
			if(channel == localChannel){
				/* if the command is coming from the local user then there is a diagram  *
				 * element queued, which is the one the user wanted to insert        	 */
				edge = (Edge)localhostDiagramElementQueue.get(diagramName).poll();
			}else{
				edge = Finder.findEdge((String)cmd.getArgAt(0),diagram.getEdgePrototypes());
				edge = (Edge)edge.clone();
				nodesToConnect = new long[cmd.getArgNum()-1];
				for(int i=0;i<nodesToConnect.length;i++){
					nodesToConnect[i] = (Long)cmd.getArgAt(i+1);
				}
			}
			/* wait for the edge to be inserted in the model so that it gets an id, which is then   *
			 * used in the awareness message to notify other users that the node has been inserted  */
			try {
				SwingUtilities.invokeAndWait(new CommandExecutor.Insert(diagram.getCollectionModel(), edge, nodesToConnect, source));
			} catch (Exception exception) {
				throw new RuntimeException(exception);
			}
			/* send the reply to the client which issued the command */
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.INSERT_EDGE_R,diagramName,"Insert new Edge", source.getLocalSource()));
			
			/* send the awareness message for edge insertion */
			DiagramEventActionSource actSource = new DiagramEventActionSource(source,
					Command.Name.INSERT_EDGE,edge.getId(),edge.getName()); 
			if(broadcastFilter.accept(actSource)){
				/* must set the username to the one of the client who sent the command       *
				 * otherwise the local username is automatically assigned in the constructor */
				for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
					if(sc.channel.equals(channel))
						actSource.setUserName(sc.userName);
				}
				
				/* process it with the broadcast filter */
				DiagramEventActionSource processedSource = broadcastFilter.process(actSource);
				
				/* since no lock is used we must send an awareness message without piggybacking */
				AwarenessMessage awMsg = new AwarenessMessage(
						AwarenessMessage.Name.START_A,
						diagramName,
						processedSource
						);
				
				if(channel != localChannel){
					/* update the local awareness panel and speech */ 
					awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), DisplayFilter.getInstance().processForText(processedSource));
					NarratorFactory.getInstance().speakWholeText(
							DisplayFilter.getInstance().processForSpeech(processedSource), Narrator.SECOND_VOICE);
				}
				/* broadcast the awareness message to all the clients but one which sent the    *
				 * command and the local one to inform them the action has started              */
				for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
					if(!sc.channel.equals(channel) && !sc.channel.equals(localChannel)){
						protocol.send(sc.channel, awMsg);
					}
				}
			}
			break;
		case REMOVE_EDGE :
			if(channel == localChannel){
				/* if the command is coming from the local user then there is  a diagram */
				/* element queued, which is the one the user wanted to insert            */
				edge = (Edge)localhostDiagramElementQueue.get(diagramName).poll();
			}else{
				diagram.getCollectionModel().getMonitor().lock();
				edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
				diagram.getCollectionModel().getMonitor().unlock();
			}
			/* wait for the Event Dispatching Thread to delete the edge, in order to avoid collisions  *
			 * with other locks, e.g. locking again an edge before the EDT deletes it                  */
			try {
				lockManager.removeLocks(edge, diagramName);
				SwingUtilities.invokeAndWait(new CommandExecutor.Remove(diagram.getCollectionModel(), edge, source));
			} catch (Exception e) {
				throw new RuntimeException(e); // must never happen
			}
			/* remove the action source, like when a lock is yielded, for this node */
			removeEventActionSource(channel,edge.getId(),diagram);
			/* send the reply to the client which issued the command */
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.REMOVE_EDGE_R,diagramName,"Edge with id "+ edge.getId() +" removed", source.getLocalSource()));
			break;
		case SET_EDGE_NAME : {
			DiagramElement de = null;
			diagram.getCollectionModel().getMonitor().lock();
			de = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)),source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_EDGE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1),source.getLocalSource()));
			}break;
		case SET_NODE_NAME : {
			DiagramElement de = null;
			diagram.getCollectionModel().getMonitor().lock();
			de = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetName(de,((String)cmd.getArgAt(1)),source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_NODE_NAME_R,diagramName,"Name set to "+ cmd.getArgAt(1),source.getLocalSource()));
			}break;	
		case SET_PROPERTY :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetProperty(
					node,
					(String)cmd.getArgAt(1),
					(Integer)cmd.getArgAt(2),
					(String)cmd.getArgAt(3),
					source
					));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(2)+ " set to "+ cmd.getArgAt(3),source.getLocalSource()));
			break;
		case SET_PROPERTIES :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			
			SwingUtilities.invokeLater(new CommandExecutor.SetProperties(
					node,
					(String)cmd.getArgAt(1),
					source
					));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_PROPERTIES_R,diagramName,"Properties for " + node.getName()+ " set to "+ cmd.getArgAt(1),source.getLocalSource()));
			break;	
		case CLEAR_PROPERTIES :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.ClearProperties(node,source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.CLEAR_PROPERTIES_R,diagramName,"Propertis of Node "+ node.getName() +" cleared",source.getLocalSource()));
			break;
		case SET_NOTES :{
			DiagramTreeNode treeNode = null;
			int[] path = new int[cmd.getArgNum()-1];
			for(int i = 0; i< cmd.getArgNum()-1;i++){
				path[i] = (Integer)cmd.getArgAt(i);
			}
			final String notes = (String)cmd.getArgAt(cmd.getArgNum()-1);
			diagram.getCollectionModel().getMonitor().lock();
			treeNode = Finder.findTreeNode(path, (DiagramTreeNode)diagram.getTreeModel().getRoot());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetNotes(diagram.getTreeModel(),treeNode,notes,source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_NOTES_R,diagramName,"Notes for " + treeNode.getName() + " successfully updated",source.getLocalSource()));
			}break;
		case ADD_PROPERTY :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.AddProperty(
					node,
					(String)cmd.getArgAt(1), 
					(String)cmd.getArgAt(2),
					source
					));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.ADD_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " added to "+ cmd.getArgAt(1),source.getLocalSource()));
			break;
		case REMOVE_PROPERTY :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.RemoveProperty(
					node, 
					(String)cmd.getArgAt(1),
					(Integer)cmd.getArgAt(2),
					source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.REMOVE_PROPERTY_R,diagramName,"Property " + cmd.getArgAt(1)+ " of type "+ cmd.getArgAt(1)+" removed",source.getLocalSource()));
			break;
		case SET_MODIFIERS :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			indexes = new HashSet<Integer>(cmd.getArgNum()-3);
			for(int i=3;i<cmd.getArgNum();i++){
				indexes.add((Integer)cmd.getArgAt(i));
			}
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetModifiers(
					node,
					(String)cmd.getArgAt(1),
					(Integer)cmd.getArgAt(2),
					indexes,
					source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_MODIFIERS_R,diagramName,"Modifiers for " + cmd.getArgAt(1)+ " successfully set",source.getLocalSource()));
			break;
		case SET_ENDLABEL :
			diagram.getCollectionModel().getMonitor().lock();
			edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
			node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetEndLabel(edge,node,(String)cmd.getArgAt(2),source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_ENDLABEL_R,diagramName,"Endlabel set to "+ cmd.getArgAt(2) +" for edge "+edge.getName(),source.getLocalSource()));
			break;
		case SET_ENDDESCRIPTION :
			diagram.getCollectionModel().getMonitor().lock();
			edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
			node = Finder.findNode((Long)cmd.getArgAt(1),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.SetEndDescription(
					edge,
					node, 
					(Integer)cmd.getArgAt(2),
					source
					));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.SET_ENDDESCRIPTION_R,diagramName,"End description set to " 
						+(cmd.getArgNum() == 3 ? cmd.getArgAt(2) : Edge.NO_ENDDESCRIPTION_STRING) 
						+ " for edge",source.getLocalSource()));
			break;	
		case TRANSLATE_NODE :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0),diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.Translate(
					node,
					new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)),
					(Double)cmd.getArgAt(3),
					(Double)cmd.getArgAt(4),
					source
					));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.TRANSLATE_NODE_R,diagramName,"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")",source.getLocalSource()));
			break;
		case TRANSLATE_EDGE :
			diagram.getCollectionModel().getMonitor().lock();
			edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.Translate(
					edge,
					new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)),
					(Double)cmd.getArgAt(3),
					(Double)cmd.getArgAt(4),
					source
					));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.TRANSLATE_EDGE_R,
						diagramName,
						"Translate. Delta=("+(Double)cmd.getArgAt(3)+","+(Double)cmd.getArgAt(4)+")",
						source.getLocalSource())
				);
			break;
		case BEND :
			diagram.getCollectionModel().getMonitor().lock();
			edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
			diagram.getCollectionModel().getMonitor().unlock();
			Point2D bendStart = null;
			if(cmd.getArgNum() == 5){
				bendStart = new Point2D.Double((Double)cmd.getArgAt(3),(Double)cmd.getArgAt(4));
			}
			SwingUtilities.invokeLater(new CommandExecutor.Bend(
					edge,
					new Point2D.Double((Double)cmd.getArgAt(1),(Double)cmd.getArgAt(2)),
					bendStart,
					source
			));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.BEND_R,diagramName,"Bend at point: ("+(Double)cmd.getArgAt(1)+","+(Double)cmd.getArgAt(1)+")",cmd.getSource().getLocalSource()));
			break;
		case STOP_EDGE_MOVE : 
			diagram.getCollectionModel().getMonitor().lock();
			edge = Finder.findEdge((Long)cmd.getArgAt(0),diagram.getCollectionModel().getEdges());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.StopMove(edge,source));
			if(channel != localChannel)
				protocol.send(channel, new Reply(Reply.Name.STOP_EDGE_MOVE_R,diagramName,"Undo straight bends",source.getLocalSource()));
			break;	
		case STOP_NODE_MOVE :
			diagram.getCollectionModel().getMonitor().lock();
			node = Finder.findNode((Long)cmd.getArgAt(0), diagram.getCollectionModel().getNodes());
			diagram.getCollectionModel().getMonitor().unlock();
			SwingUtilities.invokeLater(new CommandExecutor.StopMove(node,source));
			if(channel != localChannel){
				protocol.send(channel, new Reply(Reply.Name.STOP_NODE_MOVE_R,diagramName,"Stop node move",source.getLocalSource()));
			}
			break;
		default : throw new RuntimeException(cmd.getName().toString()+ " command not recognized");
		}
		if(broadcast){
			/* broadcast the command to all the clients but the local (uses the same model) and the one which issued the command (got a reply already)*/
			for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
				if(sc.channel != localChannel && !sc.channel.equals(channel)){
					protocol.send(sc.channel, cmd);
				}	
			}
		}
	}
	
	private void handleAwarenessMessage(AwarenessMessage awMsg,SocketChannel channel, Diagram diagram) throws IOException {
		if(diagram == null)
			synchronized(diagrams){
				diagram = diagrams.get(awMsg.getDiagram());
			}
		
		if(awMsg.getName() == AwarenessMessage.Name.ERROR_A){
			Logger.getLogger(Server.class.getCanonicalName()).info((String)awMsg.getSource());
			return;
		}
		
		/* for username aw msg checks whether the chosen name is not already used by another client.        * 
		 * If not changes the source from "newName", sent by the client, into "newName<SEPARATOR>oldName"   *
		 * in order to broadcast it to the other client and make them replace the new name with the old one */
		String oldName = ""; 
		if(awMsg.getName() == AwarenessMessage.Name.USERNAME_A){
			String userName = (String)awMsg.getSource();
			UserNameSocketChannel userNameChannel = null;
			for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
				if(sc.channel.equals(channel)){
					userNameChannel = sc;
					oldName = userNameChannel.userName;
					
				}
				/* if another user already has the name then prevent from getting it */
				if(sc.userName.equals(userName)){
					/* user name already in use, send a reply and return */
					protocol.send(channel, new AwarenessMessage(
							AwarenessMessage.Name.ERROR_A,
							awMsg.getDiagram(),
							ResourceBundle.getBundle(Server.class.getName()).getString("awareness.msg.user_already_exists")
							));
					return;
				}
			}
			userNameChannel.userName = userName;
			/* set the source of the msg for the clients, which don't hold the channel-username association         * 
			 * and therefore need a message of the form "newName<SEPARATOR>oldName" in order to do the replacement  */
			awMsg.setSource((String)awMsg.getSource()+AwarenessMessage.USERNAMES_SEPARATOR+oldName);
		}
		
		/* update the local GUI to make the local user aware of the actions */
		DisplayFilter filter = DisplayFilter.getInstance();
		if(channel != localChannel && filter != null){
			if(awMsg.getName() == AwarenessMessage.Name.USERNAME_A){
				awarenessPanelEditor.replaceUserName(awMsg.getDiagram(), (String)awMsg.getSource());
			}else{
				DiagramEventActionSource processedSource = broadcastFilter.process((DiagramEventActionSource)awMsg.getSource()); // changes according to configuration;
				if(filter.configurationHasChanged()){
					for(Diagram d : diagramChannelAllocations.keySet())
						awarenessPanelEditor.clearRecords(d.getName());
				}
				
				/* select and unselect are announced and written (temporary) on the panel, regardless START_A and STOP_A */
				if(processedSource.getCmd() == Command.Name.SELECT_NODE_FOR_EDGE_CREATION || processedSource.getCmd() == Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION){
					awarenessPanelEditor.addTimedRecord(awMsg.getDiagram(), filter.processForText(processedSource));
					NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(processedSource), Narrator.SECOND_VOICE);
				}else if(awMsg.getName() == AwarenessMessage.Name.START_A){					
					awarenessPanelEditor.addRecord(awMsg.getDiagram(), filter.processForText(processedSource));
					/* announce the just received awareness message via the second voice */
					NarratorFactory.getInstance().speakWholeText(filter.processForSpeech(processedSource), Narrator.SECOND_VOICE);
				}else{ // STOP_A
					/* selection is a timedRecord, therefore no need to remove the record on STOP_A */
					if(processedSource.getCmd() != Command.Name.SELECT_NODE_FOR_EDGE_CREATION && processedSource.getCmd() != Command.Name.UNSELECT_NODE_FOR_EDGE_CREATION)
						awarenessPanelEditor.removeRecord(awMsg.getDiagram(), filter.processForText(processedSource));
				}
			}
		}
		
		/* broadcast the awareness message to all the clients but the local and  *
		 * one which sent it, to inform them the action has started              */
		for(UserNameSocketChannel sc : diagramChannelAllocations.get(diagram)){
			if(sc.channel != localChannel && !sc.channel.equals(channel))
				protocol.send(sc.channel, awMsg);
		}
	}
	
	Map<String, Queue<DiagramElement>> getLocalhostMap() {
		return localhostDiagramElementQueue;
	}
	
	private void replyLockMessage(SocketChannel channel, String diagramName,boolean yes) throws IOException{
		protocol.send(channel, new LockMessage(
				yes ? LockMessage.Name.YES_L : LockMessage.Name.NO_L,
				diagramName,
				-1,
				DiagramEventActionSource.NULL
		));
	}
	
	private DiagramEventActionSource removeEventActionSource(SocketChannel channel, long saveID, Diagram diagram){
		Set<UserNameSocketChannel> userNames = diagramChannelAllocations.get(diagram);
		if(userNames != null){
			for(UserNameSocketChannel userName : userNames){
				if(userName.channel.equals(channel)){
					for(DiagramEventActionSource s : userName.lockAwarenessSources){
						if(s.getSaveID() == saveID){
							userName.lockAwarenessSources.remove(s);
							return s;
						}
					}
				}
			}
		}
		return null;
	}
	
	private Set<Integer> indexes;
	/* the String key is the name of diagram, this collection is shared with the class Server * 
	 * and it's used to retrieve the diagrams out of commands                                 */
	private Map<String,Diagram> diagrams;
	/* unique localChannel for all the digrams */
	private SocketChannel localChannel;
	/* this map contains all the channels bound to a diagram, so if a change  	* 
	 * is made to a diagram all its channels are broadcasted through this map   */
	private Map<Diagram,Set<UserNameSocketChannel>> diagramChannelAllocations;
	/* this map is used to pass the reference to elements created by the local client         *  
	 * (we don't create a new object as well as we do for the nodes created by remote clients */
	private Map<String, Queue<DiagramElement>> localhostDiagramElementQueue;
	private Protocol protocol;
	private ServerLockManager lockManager;
	private BroadcastFilter broadcastFilter;
	private AwarenessPanelEditor awarenessPanelEditor; 
	
	/* this class holds for each socketChannel the username associated to it 
	 * and the last received awareness message source,*/
	private static class UserNameSocketChannel {
		UserNameSocketChannel(SocketChannel channel){
			this(channel,"");
		}
		
		UserNameSocketChannel(SocketChannel channel, String userName){
			this.channel = channel;
			this.userName = userName;
			lockAwarenessSources = new LinkedList<DiagramEventActionSource>();
		}
		
		SocketChannel channel;
		String userName;
		List<DiagramEventActionSource> lockAwarenessSources;
	}
}