f@0: /*
f@0: CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
f@0:
f@0: Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
f@0:
f@0: This program is free software: you can redistribute it and/or modify
f@0: it under the terms of the GNU General Public License as published by
f@0: the Free Software Foundation, either version 3 of the License, or
f@0: (at your option) any later version.
f@0:
f@0: This program is distributed in the hope that it will be useful,
f@0: but WITHOUT ANY WARRANTY; without even the implied warranty of
f@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f@0: GNU General Public License for more details.
f@0:
f@0: You should have received a copy of the GNU General Public License
f@0: along with this program. If not, see .
f@0: */
f@0:
f@0: package uk.ac.qmul.eecs.ccmi.network;
f@0:
f@0: import java.io.Closeable;
f@0: import java.io.IOException;
f@0: import java.net.InetSocketAddress;
f@0: import java.nio.channels.SelectionKey;
f@0: import java.nio.channels.Selector;
f@0: import java.nio.channels.ServerSocketChannel;
f@0: import java.nio.channels.SocketChannel;
f@0: import java.util.HashMap;
f@0: import java.util.Iterator;
f@0: import java.util.Map;
f@0: import java.util.Queue;
f@0: import java.util.ResourceBundle;
f@0: import java.util.concurrent.ConcurrentLinkedQueue;
f@0: import java.util.logging.Handler;
f@0: import java.util.logging.Level;
f@0: import java.util.logging.LogRecord;
f@0: import java.util.logging.Logger;
f@0:
f@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
f@0: import uk.ac.qmul.eecs.ccmi.gui.Diagram;
f@0: import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;
f@0: import uk.ac.qmul.eecs.ccmi.utils.PreferencesService;
f@0:
f@0: /**
f@0: * The {@code Server} is a thread the user can start, which accept connections from other clients
f@0: * (other users machines running the CCmI editor).
f@0: * When a user shares a diagram via the server, other clients
f@0: * will be able to see and to modify it in real time. This is achieved by exchanging network
f@0: * message between the server and each client.
f@0: * On a shared diagram scenario the server
f@0: * diagram model is considered to be the "real" one. That is, the other clients will continuously
f@0: * synchronize their model with the server and even before applying a command issued by an action of
f@0: * the local user, clients need first send the command to the server (which will apply the command on its own model)
f@0: * and wait for an acknowledging reply.
f@0: *
f@0: */
f@0: public class Server extends NetworkThread {
f@0:
f@0: /**
f@0: * Create a new server instance. If another instance is already running then it's shut
f@0: * down before the new instance is created.
f@0: *
f@0: * @return a {@code Server} instance.
f@0: */
f@0: public static Server createServer(){
f@0: if(server != null)
f@0: server.shutdown(server.resources.getString("log.restart"));
f@0: server = new Server();
f@0: return server;
f@0: }
f@0:
f@0: public static Server getServer(){
f@0: return server;
f@0: }
f@0:
f@0: private Server(){
f@0: super("Server Thread");
f@0: mustSayGoodbye = false;
f@0: initialized = false;
f@0: resources = ResourceBundle.getBundle(this.getClass().getName());
f@0: }
f@0:
f@0: public void init() throws IOException {
f@0: if(initialized)
f@0: return;
f@0: serverChannel = ServerSocketChannel.open();
f@0: selector = Selector.open();
f@0: diagrams = new HashMap();
f@0: scManager = new ServerConnectionManager(diagrams,getAwarenessPanelEditor());
f@0: String portAsString = PreferencesService.getInstance().get("server.local_port",Server.DEFAULT_LOCAL_PORT);
f@0: int port = Integer.parseInt(portAsString);
f@0: serverChannel.socket().bind(new InetSocketAddress(port));
f@0: serverChannel.configureBlocking(false);
f@0: serverChannel.register(selector, SelectionKey.OP_ACCEPT);
f@0: Handler serverLogHandler = new Handler(){
f@0: @Override
f@0: public void close() throws SecurityException {}
f@0:
f@0: @Override
f@0: public void flush() {}
f@0:
f@0: @Override
f@0: public void publish(LogRecord record) {
f@0: NarratorFactory.getInstance().speakWholeText(record.getMessage());
f@0: }
f@0: };
f@0: serverLogHandler.setLevel(Level.CONFIG);
f@0: logger.addHandler(serverLogHandler);
f@0: logger.config("Server initialized, will listen on port: "+port);
f@0: initialized = true;
f@0: }
f@0:
f@0: @Override
f@0: public void run(){
f@0: logger.config(resources.getString("log.start"));
f@0: running = true;
f@0: while(!mustSayGoodbye){
f@0: try{
f@0: selector.select();
f@0: if(mustSayGoodbye)
f@0: break;
f@0: for (Iterator itr = selector.selectedKeys().iterator(); itr.hasNext();){
f@0: SelectionKey key = itr.next();
f@0: itr.remove();
f@0:
f@0: if(!key.isValid())
f@0: continue;
f@0: if(key.isAcceptable()){
f@0: SocketChannel clientChannel = serverChannel.accept();
f@0: clientChannel.configureBlocking(false);
f@0: clientChannel.register(selector, SelectionKey.OP_READ);
f@0: /* log connection only if it's not from the local channel */
f@0: if(!"/127.0.0.1".equals(clientChannel.socket().getInetAddress().toString()))
f@0: logger.info(resources.getString("log.client_connected"));
f@0: }
f@0: if(key.isReadable()){
f@0: try{
f@0: scManager.handleMessage((SocketChannel)key.channel());
f@0: }catch(IOException ioe){
f@0: /* Upon exception the channel is no longer considered reliable: it's cleaned up and closed */
f@0: SocketChannel channel = (SocketChannel)key.channel();
f@0: channel.close();
f@0: scManager.removeChannel(channel);
f@0: logger.info(ioe.getLocalizedMessage());
f@0: }
f@0: }
f@0: }
f@0: }catch(IOException e){
f@0: logger.severe(e.getLocalizedMessage());
f@0: e.printStackTrace();
f@0: break;
f@0: }
f@0: }
f@0: cleanup();
f@0: }
f@0:
f@0: private void cleanup(){
f@0: try {
f@0: /* close all the channels */
f@0: serverChannel.close();
f@0: for (Iterator itr = selector.keys().iterator(); itr.hasNext();){
f@0: SelectionKey key = itr.next();
f@0: ((Closeable)key.channel()).close();
f@0: }
f@0: } catch (IOException e) {
f@0: e.printStackTrace();
f@0: }
f@0: running = false;
f@0: }
f@0:
f@0: public void shutdown(String msg){
f@0: if(msg != null)
f@0: NarratorFactory.getInstance().speak(resources.getString("log.shutdown")+msg);
f@0: shutdown();
f@0: }
f@0:
f@0: public void shutdown(){
f@0: mustSayGoodbye = true;
f@0: selector.wakeup();
f@0: server = null;
f@0: for(Handler h : logger.getHandlers()){
f@0: h.close();
f@0: logger.removeHandler(h);
f@0: }
f@0: }
f@0:
f@0: public Queue getLocalhostQueue(String diagramName){
f@0: return scManager.getLocalhostMap().get(diagramName);
f@0: }
f@0:
f@0: /* this is called by the event dispatching thread when the user shares a diagram */
f@0: public void share(Diagram diagram) throws ServerNotRunningException, DiagramAlreadySharedException{
f@0: String name = diagram.getName();
f@0: if(!running)
f@0: throw new ServerNotRunningException(resources.getString("exception_msg.not_run"));
f@0: synchronized(diagrams){
f@0: if(diagrams.containsKey(name)){
f@0: throw new DiagramAlreadySharedException(resources.getString("exception_msg.already_shared"));
f@0: }
f@0: diagrams.put(name,diagram);
f@0: }
f@0: scManager.getLocalhostMap().put(name, new ConcurrentLinkedQueue());
f@0: logger.info("Diagram "+name+" shared on the server");
f@0: }
f@0:
f@0: private static Server server;
f@0: public static final String DEFAULT_LOCAL_PORT = "7777";
f@0: public static final String DEFAULT_REMOTE_PORT = "7777";
f@0: static Logger logger;
f@0: private ServerSocketChannel serverChannel;
f@0: private ServerConnectionManager scManager;
f@0: private Selector selector;
f@0: private Map diagrams;
f@0: private ResourceBundle resources;
f@0: private boolean initialized;
f@0: private volatile boolean mustSayGoodbye;
f@0: private boolean running;
f@0:
f@0: static {
f@0: logger = Logger.getLogger(Server.class.getName());
f@0: logger.setUseParentHandlers(false);
f@0: logger.setLevel(Level.CONFIG);
f@0: }
f@0: }