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: }