package edu.harvard.econcs.turkserver.client; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import java.util.logging.Logger; import org.cometd.annotation.*; import org.cometd.bayeux.Channel; import org.cometd.bayeux.Message; import org.cometd.bayeux.client.ClientSession; import org.cometd.bayeux.client.ClientSessionChannel; import org.cometd.bayeux.client.ClientSessionChannel.MessageListener; import org.cometd.client.BayeuxClient; import org.cometd.client.ext.AckExtension; import org.cometd.client.ext.TimesyncClientExtension; import org.cometd.websocket.client.WebSocketTransport; import org.eclipse.jetty.websocket.WebSocketClientFactory; import edu.harvard.econcs.turkserver.Codec; import edu.harvard.econcs.turkserver.api.ClientController; public class SessionClient<C> implements ClientController { static final WebSocketClientFactory wsFactory = new WebSocketClientFactory(); protected Logger logger; private String hitId; private String assignmentId; private String workerId; protected String expBroadcastChan; protected String expServiceChan; private MessageListener broadcastListener = null; private MessageListener serviceListener = null; protected ClientAnnotationManager<C> clientWrapper; protected BayeuxClient bayeuxClient; protected ClientAnnotationProcessor processor; private UserClientService ucl; private volatile boolean connected; private volatile boolean wasConnected; private volatile boolean isError = false; protected SessionClient() { } public C getClientBean() { return clientWrapper.clientBean; } /* ******************************************** * ClientController methods **********************************************/ @Override public String getHitId() { return hitId; } @Override public String getAssignmentId() { return assignmentId; } @Override public String getWorkerId() { return workerId; } @Override public String getUsername() { // TODO Auto-generated method stub return null; } @Override public boolean isConnected() { return bayeuxClient == null ? connected : bayeuxClient.isConnected(); } public boolean isError() { return isError; } public void connect(String url, String hitId, String assignmentId, String workerId) { if( bayeuxClient != null ) throw new RuntimeException("Already attempted a connect!"); this.hitId = hitId; this.assignmentId = assignmentId; this.workerId = workerId; logger = Logger.getLogger(this.getClass().getSimpleName() + this.getHitId()); // LongPollingTransport.create(null) bayeuxClient = new BayeuxClient(url, WebSocketTransport.create(null, wsFactory)); bayeuxClient.addExtension(new TimesyncClientExtension()); bayeuxClient.addExtension(new AckExtension()); processor = new ClientAnnotationProcessor(bayeuxClient); processor.process(ucl = new UserClientService()); logger.info("Attempting connection with ID " + getHitId()); bayeuxClient.handshake(); // Start a thread that will watch for connection success or failure new Thread() { public void run() { if( !bayeuxClient.waitFor(2000, BayeuxClient.State.CONNECTED) ) { handShakeFail(); return; } } }.start(); } public void disconnect() { if( bayeuxClient == null || !bayeuxClient.isConnected() ) return; // unsubscribe from any experiment channel too if( broadcastListener != null ) { bayeuxClient.getChannel(expBroadcastChan).unsubscribe(broadcastListener); broadcastListener = null; } if( serviceListener != null ) { bayeuxClient.getChannel(expServiceChan).removeListener(serviceListener); serviceListener = null; } processor.deprocess(ucl); try { bayeuxClient.disconnect(); } catch( Exception e ) { logger.info("Ignoring thrown exception on disconnect attempt"); } } @Override public void sendQuizResults(QuizResults qr) { Map<String, Object> quizResults = new TreeMap<String, Object>(); quizResults.put("status", "quizresults"); quizResults.put("correct", qr.correct); quizResults.put("total", qr.total); bayeuxClient.getChannel("/service/user").publish(quizResults); } @Override public void sendExperimentBroadcast(Map<String, Object> data) { bayeuxClient.getChannel(expBroadcastChan).publish(data); } @Override public void sendExperimentService(Map<String, Object> data) { bayeuxClient.getChannel(expServiceChan).publish(data); } public void subscribeExpChannel(String chan) { expBroadcastChan = Codec.expChanPrefix + chan; logger.info("Subscribing to exp broadcast channel: " + expBroadcastChan); expServiceChan = Codec.expSvcPrefix + chan; logger.info("Subscribing to exp service channel: " + expServiceChan); broadcastListener = new MessageListener() { @Override public void onMessage(ClientSessionChannel channel, Message message) { clientWrapper.deliverBroadcast(message.getDataAsMap()); } }; serviceListener = new MessageListener() { @Override public void onMessage(ClientSessionChannel channel, Message message) { clientWrapper.deliverService(message.getDataAsMap()); } }; bayeuxClient.getChannel(expBroadcastChan).subscribe(broadcastListener); // Don't use addListener here...gives random useless messages bayeuxClient.getChannel(expServiceChan).subscribe(serviceListener); } @Service private class UserClientService { @Session private ClientSession client; @Listener(Channel.META_CONNECT) public void metaConnect(Message connectMsg) { if (bayeuxClient.isDisconnected()) { connected = false; connectionClosed(); return; } wasConnected = connected; connected = connectMsg.isSuccessful(); if (!wasConnected && connected) { connectionEstablished(); } else if (wasConnected && !connected) { connectionBroken(); } } @Listener(Channel.META_HANDSHAKE) public void metaHandshake(Message handshake) { if( handshake.isSuccessful() ) { connectionInitialized(); // Succeeded...just add a disconnect hook. Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { logger.info("Disconnecting Bayeux client"); disconnect(); } }); } else { handShakeFail(); } } @Listener(Channel.META_DISCONNECT) public void metaDisconnect(Message disconnect) { // TODO: this doesn't seem to be working with websocket if( disconnect.isSuccessful() ) { connected = false; } } @Subscription("/service/user") public void serviceUser(Message service) { Map<String, Object> m = service.getDataAsMap(); Object status = m.get("status"); if( status != null ) { if( Codec.status_expfinished.equals(status.toString() ) ) { logger.info("Connected to experiment that is already done"); clientWrapper.triggerClientError(Codec.status_expfinished); disconnect(); } else if( Codec.status_connectexp.equals(status.toString())) { // Subscribe to this channel String chan = m.get("channel").toString(); subscribeExpChannel(chan); clientWrapper.triggerStartExperiment(); } else if( Codec.roundStartMsg.equals(status.toString())) { clientWrapper.triggerStartRound(((Number) m.get("round")).intValue()); } else if( Codec.doneExpMsg.equals(status.toString())) { clientWrapper.triggerFinishExperiment(); // Do nothing } else if( Codec.status_batchfinished.equals(status.toString())) { clientWrapper.triggerClientError(Codec.status_batchfinished); disconnect(); } else if( Codec.status_error.equals(status.toString()) ) { clientWrapper.triggerClientError(m.get("msg").toString()); isError = true; } else if( Codec.status_completed.equals(status.toString()) ) { System.out.println("Got complete confirmation, disconnecting."); disconnect(); } } else { System.out.println("Service message unexpected:"); System.out.println(m); } } } /* **************************************************** * Connection status handlers, can be overridden by subclasses ******************************************************/ protected void handShakeFail() { System.out.println("BayeuxClient Failed Handshake."); } /** * First time connection is initialized */ protected void connectionInitialized() { System.out.println(bayeuxClient.getId() + " initialized"); } /** * Connection established, could be after first time */ protected void connectionEstablished() { System.out.println(bayeuxClient.getId() + " established"); Map<String, Object> data = new HashMap<String, Object>(); if( assignmentId != null && !assignmentId.equals("ASSIGNMENT_ID_NOT_AVAILABLE") ) { data.put("status", Codec.hitAccept); data.put("hitId", hitId); data.put("assignmentId", assignmentId); data.put("workerId", workerId); System.out.println(bayeuxClient.getId() + " sending accept"); } else { data.put("status", Codec.hitView); data.put("hitId", hitId); System.out.println(bayeuxClient.getId() + " sending view"); } bayeuxClient.getChannel("/service/user").publish(data); } /** * Connection broken */ protected void connectionBroken() { } /** * Disconnected (either by server or by client) */ protected void connectionClosed() { } public void submit(String comments) { Map<String, Object> m = new HashMap<String, Object>(); m.put("status", Codec.hitSubmit); m.put("comments", comments); bayeuxClient.getChannel("/service/user").publish(m); } @Override public void recordInactivity(long timeInactive) { // TODO Auto-generated method stub } }