view java/src/uk/ac/qmul/eecs/ccmi/network/Server.java @ 0:78b7fc5391a2

first import, outcome of NIME 2014 hackaton
author Fiore Martin <f.martin@qmul.ac.uk>
date Tue, 08 Jul 2014 16:28:59 +0100
parents
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.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
import uk.ac.qmul.eecs.ccmi.gui.Diagram;
import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;
import uk.ac.qmul.eecs.ccmi.utils.PreferencesService;

/**
 * The {@code Server} is a thread the user can start, which accept connections from other clients 
 * (other users machines running the CCmI editor).
 * When a user shares a diagram via the server, other clients 
 * will be able to see and to modify it in real time. This is achieved by exchanging network
 * message between the server and each client.   
 * On a shared diagram scenario the server
 * diagram model is considered to be the "real" one. That is, the other clients will continuously 
 * synchronize their model with the server and even before applying a command issued by an action of 
 * the local user, clients need first send the command to the server (which will apply the command on its own model) 
 * and wait for an acknowledging reply. 
 *
 */
public class Server extends NetworkThread {
	
	/**
	 * Create a new server instance. If another instance is already running then it's shut
	 * down before the new instance is created. 
	 * 
	 * @return a {@code Server} instance.
	 */
	public static Server createServer(){
		if(server != null)
			server.shutdown(server.resources.getString("log.restart"));
		server = new Server();
		return server;
	}
	
	public static Server getServer(){
		return server;
	}
	
	private Server(){
		super("Server Thread");
		mustSayGoodbye = false;
		initialized = false;
		resources = ResourceBundle.getBundle(this.getClass().getName());
	}
		
	public void init() throws IOException {
		if(initialized)
			return;
		serverChannel = ServerSocketChannel.open();
		selector = Selector.open();
		diagrams = new HashMap<String,Diagram>();
		scManager = new ServerConnectionManager(diagrams,getAwarenessPanelEditor());
		String portAsString = PreferencesService.getInstance().get("server.local_port",Server.DEFAULT_LOCAL_PORT);
		int port = Integer.parseInt(portAsString);
		serverChannel.socket().bind(new InetSocketAddress(port));
		serverChannel.configureBlocking(false);
		serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		Handler serverLogHandler = new Handler(){
			@Override
			public void close() throws SecurityException {}

			@Override
			public void flush() {}

			@Override
			public void publish(LogRecord record) {
				NarratorFactory.getInstance().speakWholeText(record.getMessage());
			}
		};
		serverLogHandler.setLevel(Level.CONFIG);
		logger.addHandler(serverLogHandler);
		logger.config("Server initialized, will listen on port: "+port);
		initialized = true;
	}
	
	@Override
	public void run(){
		logger.config(resources.getString("log.start"));
		running = true;
		while(!mustSayGoodbye){
			try{
				selector.select();
				if(mustSayGoodbye)
					break;
				for (Iterator<SelectionKey> itr = selector.selectedKeys().iterator(); itr.hasNext();){
					SelectionKey key = itr.next(); 
					itr.remove(); 
					
					if(!key.isValid())
						continue;
					if(key.isAcceptable()){
						SocketChannel clientChannel = serverChannel.accept();
						clientChannel.configureBlocking(false);
						clientChannel.register(selector, SelectionKey.OP_READ);
						/* log connection only if it's not from the local channel */
						if(!"/127.0.0.1".equals(clientChannel.socket().getInetAddress().toString()))
							logger.info(resources.getString("log.client_connected"));
					}
					if(key.isReadable()){
						try{
							scManager.handleMessage((SocketChannel)key.channel());
						}catch(IOException ioe){
							/* Upon exception the channel is no longer considered reliable: it's cleaned up and closed */
							SocketChannel channel = (SocketChannel)key.channel(); 
							channel.close();
							scManager.removeChannel(channel);
							logger.info(ioe.getLocalizedMessage());
						}
					}
				}
			}catch(IOException e){
				logger.severe(e.getLocalizedMessage());
				e.printStackTrace();
				break;
			}
		}
		cleanup();
	}
	
	private void cleanup(){	
		try {
			/* close all the channels */
			serverChannel.close();
			for (Iterator<SelectionKey> itr = selector.keys().iterator(); itr.hasNext();){
				SelectionKey key = itr.next();
				((Closeable)key.channel()).close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		running = false;
	}
	
	public void shutdown(String msg){
		if(msg != null)
			NarratorFactory.getInstance().speak(resources.getString("log.shutdown")+msg);
		shutdown();
	}
	
	public void shutdown(){
		mustSayGoodbye = true;
		selector.wakeup();
		server = null;
		for(Handler h : logger.getHandlers()){
			h.close();
			logger.removeHandler(h);
		}
	}
	
	public Queue<DiagramElement> getLocalhostQueue(String diagramName){
		return scManager.getLocalhostMap().get(diagramName);
	}
	
	/* this is called by the event dispatching thread when the user shares a diagram */
	public void share(Diagram diagram) throws ServerNotRunningException, DiagramAlreadySharedException{
		String name = diagram.getName();
		if(!running)
			throw new ServerNotRunningException(resources.getString("exception_msg.not_run"));
		synchronized(diagrams){
			if(diagrams.containsKey(name)){
				throw new DiagramAlreadySharedException(resources.getString("exception_msg.already_shared"));
			}
			diagrams.put(name,diagram);
		}
		scManager.getLocalhostMap().put(name, new ConcurrentLinkedQueue<DiagramElement>());
		logger.info("Diagram "+name+" shared on the server");
	}
	
	private static Server server;
	public static final String DEFAULT_LOCAL_PORT = "7777";
	public static final String DEFAULT_REMOTE_PORT = "7777";
	static Logger logger;
	private ServerSocketChannel serverChannel;
	private ServerConnectionManager scManager;
	private Selector selector;
	private Map<String, Diagram> diagrams;
	private ResourceBundle resources;
	private boolean initialized;
	private volatile boolean mustSayGoodbye;
	private boolean running;	
	
	static {
		logger = Logger.getLogger(Server.class.getName());
		logger.setUseParentHandlers(false);
		logger.setLevel(Level.CONFIG);
	}
}