diff --git a/src/main/java/de/thm/arsnova/services/ISessionService.java b/src/main/java/de/thm/arsnova/services/ISessionService.java index 6efdcfc9db7e1818b0d8c7528360028584ed649f..4e353c774f7749f6cd61993da44c66b7186d8ef5 100644 --- a/src/main/java/de/thm/arsnova/services/ISessionService.java +++ b/src/main/java/de/thm/arsnova/services/ISessionService.java @@ -27,6 +27,7 @@ import de.thm.arsnova.entities.User; public interface ISessionService { + public void cleanFeedbackVotes(); public Session getSession(String keyword); public Session saveSession(Session session); public Feedback getFeedback(String keyword); diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java index d113764f420269f8a4a03dd42e91c373c307b270..85555e921e2ebcec1a967f4976dca18498dbd6fc 100644 --- a/src/main/java/de/thm/arsnova/services/SessionService.java +++ b/src/main/java/de/thm/arsnova/services/SessionService.java @@ -19,9 +19,15 @@ package de.thm.arsnova.services; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; @@ -31,6 +37,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; @@ -45,6 +52,7 @@ import com.fourspaces.couchdb.ViewResults; import de.thm.arsnova.entities.Feedback; import de.thm.arsnova.entities.Session; +import de.thm.arsnova.socket.ARSnovaSocketIOServer; @Service public class SessionService implements ISessionService { @@ -52,6 +60,9 @@ public class SessionService implements ISessionService { @Autowired IUserService userService; + @Autowired + ARSnovaSocketIOServer server; + private String databaseHost; private int databasePort; private String databaseName; @@ -76,6 +87,72 @@ public class SessionService implements ISessionService { public final void setDatabaseName(String databaseName) { this.databaseName = databaseName; } + + /** + * This method cleans up old feedback votes at the scheduled interval. + */ + @Override + @Scheduled(fixedDelay=5000) + public void cleanFeedbackVotes() { + final long timelimitInMillis = /*10 * 60 **/ 10000; + final long maxAllowedTimeInMillis = System.currentTimeMillis() - timelimitInMillis; + + Map<String, Set<String>> affectedUsers = new HashMap<String, Set<String>>(); + Set<String> allAffectedSessions = new HashSet<String>(); + + List<Document> results = findFeedbackForDeletion(maxAllowedTimeInMillis); + for (Document d : results) { + try { + // Read the required document data + Document feedback = this.getDatabase().getDocument(d.getId()); + String arsInternalSessionId = feedback.getString("sessionId"); + String user = feedback.getString("user"); + + // Store user and session data for later. We need this to communicate the changes back to the users. + Set<String> affectedArsSessions = affectedUsers.get(user); + if (affectedArsSessions == null) { + affectedArsSessions = new HashSet<String>(); + } + affectedArsSessions.add(getSessionKeyword(arsInternalSessionId)); + affectedUsers.put(user, affectedArsSessions); + allAffectedSessions.addAll(affectedArsSessions); + + this.database.deleteDocument(feedback); + logger.debug("Cleaning up Feedback document " + d.getId()); + } catch (IOException e) { + logger.error("Could not delete Feedback document " + d.getId()); + } + } + if (!results.isEmpty()) { + broadcastFeedbackChanges(affectedUsers, allAffectedSessions); + } + } + + /** + * + * @param affectedUsers The user whose feedback got deleted along with all affected session keywords + * @param allAffectedSessions For convenience, this represents the union of all session keywords mentioned above. + */ + private void broadcastFeedbackChanges(Map<String, Set<String>> affectedUsers, Set<String> allAffectedSessions) { + for (Map.Entry<String, Set<String>> e : affectedUsers.entrySet()) { + // Is this user registered with a socket connection? + String connectedSocket = user2session.get(e.getKey()); + if (connectedSocket != null) { + this.server.reportDeletedFeedback(e.getKey(), e.getValue()); + } + } + this.server.reportUpdatedFeedbackForSessions(allAffectedSessions); + } + + private List<Document> findFeedbackForDeletion(final long maxAllowedTimeInMillis) { + View cleanupFeedbackView = new View("understanding/cleanup"); + cleanupFeedbackView.setStartKey("null"); + cleanupFeedbackView.setEndKey(String.valueOf(maxAllowedTimeInMillis)); + ViewResults feedbackForCleanup = this.getDatabase().view(cleanupFeedbackView); + return feedbackForCleanup.getResults(); + } + + @Override public Session getSession(String keyword) { @@ -174,28 +251,30 @@ public class SessionService implements ISessionService { public boolean postFeedback(String keyword, int value, de.thm.arsnova.entities.User user) { String sessionId = this.getSessionId(keyword); if (sessionId == null) return false; + if (!(value >= 0 && value <= 3)) return false; Document feedback = new Document(); - feedback.put("type", "understanding"); - feedback.put("user", user.getUsername()); - feedback.put("sessionId", sessionId); - feedback.put("timestamp", System.currentTimeMillis()); + List<Document> postedFeedback = findPreviousFeedback(sessionId, user); - switch (value) { - case 0: - feedback.put("value", "Bitte schneller"); - break; - case 1: - feedback.put("value", "Kann folgen"); - break; - case 2: - feedback.put("value", "Zu schnell"); + // Feedback can only be posted once. If there already is some feedback, we need to update it. + if (!postedFeedback.isEmpty()) { + for (Document f : postedFeedback) { + // Use the first found feedback and update value and timestamp + try { + feedback = this.getDatabase().getDocument(f.getId()); + feedback.put("value", feedbackValueToString(value)); + feedback.put("timestamp", System.currentTimeMillis()); + } catch (IOException e) { + return false; + } break; - case 3: - feedback.put("value", "Nicht mehr dabei"); - break; - default: - return false; + } + } else { + feedback.put("type", "understanding"); + feedback.put("user", user.getUsername()); + feedback.put("sessionId", sessionId); + feedback.put("timestamp", System.currentTimeMillis()); + feedback.put("value", feedbackValueToString(value)); } try { @@ -206,6 +285,32 @@ public class SessionService implements ISessionService { return true; } + + private List<Document> findPreviousFeedback(String sessionId, de.thm.arsnova.entities.User user) { + View view = new View("understanding/by_user"); + try { + view.setKey(URLEncoder.encode("[\"" + sessionId + "\", \"" + user.getUsername() + "\"]", "UTF-8")); + } catch(UnsupportedEncodingException e) { + return Collections.<Document>emptyList(); + } + ViewResults results = this.getDatabase().view(view); + return results.getResults(); + } + + private String feedbackValueToString(int value) { + switch (value) { + case 0: + return "Bitte schneller"; + case 1: + return "Kann folgen"; + case 2: + return "Zu schnell"; + case 3: + return "Nicht mehr dabei"; + default: + return ""; + } + } @Override @Transactional(isolation=Isolation.READ_COMMITTED) @@ -252,6 +357,23 @@ public class SessionService implements ISessionService { return results.getJSONArray("rows").optJSONObject(0) .optJSONObject("value").getString("_id"); } + + private String getSessionKeyword(String internalSessionId) { + try { + View view = new View("session/by_id"); + view.setKey(URLEncoder.encode("\"" + internalSessionId + "\"", "UTF-8")); + ViewResults results = this.getDatabase().view(view); + for (Document d : results.getResults()) { + Document arsSession = this.getDatabase().getDocument(d.getId()); + return arsSession.get("keyword").toString(); + } + } catch (UnsupportedEncodingException e) { + return ""; + } catch (IOException e) { + return ""; + } + return ""; + } private String currentTimestamp() { return Long.toString(System.currentTimeMillis()); diff --git a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java index 93e061eee6b81ea43d47937ba3403ec789da8672..eb7f2afb1cee995976df7f110bfbb85de8d6587b 100644 --- a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java +++ b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java @@ -5,19 +5,26 @@ import java.io.FileNotFoundException; import java.io.InputStream; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang.NotImplementedException; +import org.apache.http.MethodNotSupportedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Required; +import com.corundumstudio.socketio.AckCallback; +import com.corundumstudio.socketio.ClientOperations; import com.corundumstudio.socketio.Configuration; import com.corundumstudio.socketio.SocketIOClient; import com.corundumstudio.socketio.SocketIOServer; import com.corundumstudio.socketio.listener.ConnectListener; import com.corundumstudio.socketio.listener.DataListener; import com.corundumstudio.socketio.listener.DisconnectListener; +import com.corundumstudio.socketio.parser.Packet; import de.thm.arsnova.entities.User; import de.thm.arsnova.services.ISessionService; @@ -161,4 +168,96 @@ public class ARSnovaSocketIOServer { public boolean authorize(String session, User user) { return session2user.put(session, user) != null; } + + public void reportDeletedFeedback(String username, Set<String> arsSessions) { + String connectionId = findConnectionIdForUser(username); + if (connectionId == "") { + return; + } + + UUID connectionUuid = UUID.fromString(connectionId); + for (SocketIOClient client : server.getAllClients()) { + // Find the client whose feedback has been deleted and send a message. + if (client.getSessionId().compareTo(connectionUuid) == 0) { + ClientOperations clientOp = new ARSnovaClientOperations(client); + clientOp.sendEvent("removedFeedback", arsSessions); + break; + } + } + } + + private String findConnectionIdForUser(String username) { + String connectionId = ""; + for (Map.Entry<String, User> e : session2user.entrySet()) { + User u = e.getValue(); + if (u.getUsername().equals(username)) { + connectionId = e.getKey(); + break; + } + } + return connectionId; + } + + + public void reportUpdatedFeedbackForSessions(Set<String> allAffectedSessions) { + for (String sessionKey : allAffectedSessions) { + de.thm.arsnova.entities.Feedback fb = sessionService.getFeedback(sessionKey); + server.getBroadcastOperations().sendEvent("updateFeedback", fb.getValues()); + } + } + + private static class ARSnovaClientOperations implements ClientOperations { + + private final SocketIOClient client; + + public ARSnovaClientOperations(SocketIOClient client) { + this.client = client; + } + + @Override + public void disconnect() { + throw new NotImplementedException(); + } + + @Override + public void send(Packet arg0) { + throw new NotImplementedException(); + } + + @Override + public void send(Packet arg0, AckCallback arg1) { + throw new NotImplementedException(); + } + + @Override + public void sendEvent(String eventName, Object data) { + client.sendEvent(eventName, data); + } + + @Override + public void sendEvent(String arg0, Object arg1, AckCallback arg2) { + throw new NotImplementedException(); + } + + @Override + public void sendJsonObject(Object arg0) { + throw new NotImplementedException(); + } + + @Override + public void sendJsonObject(Object arg0, AckCallback arg1) { + throw new NotImplementedException(); + } + + @Override + public void sendMessage(String arg0) { + throw new NotImplementedException(); + } + + @Override + public void sendMessage(String arg0, AckCallback arg1) { + throw new NotImplementedException(); + } + + } } \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/api-servlet.xml b/src/main/webapp/WEB-INF/api-servlet.xml index b7865697175d00f1f3a08b1aa6d059e90777b0cf..345e7710c3025c033b0669bda35c8a7e8cd16e29 100644 --- a/src/main/webapp/WEB-INF/api-servlet.xml +++ b/src/main/webapp/WEB-INF/api-servlet.xml @@ -3,15 +3,18 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security" xmlns:mvc="http://www.springframework.org/schema/mvc" + xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd - http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd + http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.1.xsd"> <context:component-scan base-package="de.thm.arsnova" /> <context:annotation-config /> <mvc:annotation-driven /> + <task:annotation-driven /> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />