diff --git a/pom.xml b/pom.xml index d09601cca31f8960157a084665aa50dac2f24a0c..4370e8e6ef88bdb0e5815b477681b21dbfad18e9 100644 --- a/pom.xml +++ b/pom.xml @@ -215,6 +215,7 @@ <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework-version}</version> + <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> @@ -239,6 +240,12 @@ <artifactId>netty-socketio</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + <version>1</version> + <scope>test</scope> + </dependency> </dependencies> <build> <plugins> @@ -275,6 +282,13 @@ <locales>en</locales> </configuration> </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.12.2</version> + <configuration> + </configuration> + </plugin> </plugins> </build> diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java new file mode 100644 index 0000000000000000000000000000000000000000..a799fc4f1874258b5c794ed6bd9590ac67f63e01 --- /dev/null +++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2012 THM webMedia + * + * This file is part of ARSnova. + * + * ARSnova 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. + * + * ARSnova 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 de.thm.arsnova.dao; + +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 net.sf.json.JSONException; +import net.sf.json.JSONObject; + +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.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import com.fourspaces.couchdb.Database; +import com.fourspaces.couchdb.Document; +import com.fourspaces.couchdb.View; +import com.fourspaces.couchdb.ViewResults; + +import de.thm.arsnova.entities.Feedback; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; +import de.thm.arsnova.services.ISessionService; +import de.thm.arsnova.services.IUserService; + +@Component +public class CouchDBDao implements IDatabaseDao { + @Autowired + IUserService userService; + + @Autowired + ISessionService sessionService; + + private String databaseHost; + private int databasePort; + private String databaseName; + + private Database database; + + // + + public static final Logger logger = LoggerFactory.getLogger(CouchDBDao.class); + + @Value("${couchdb.host}") + public final void setDatabaseHost(String databaseHost) { + logger.info(databaseHost); + this.databaseHost = databaseHost; + } + + @Value("${couchdb.port}") + public final void setDatabasePort(String databasePort) { + logger.info(databasePort); + this.databasePort = Integer.parseInt(databasePort); + } + + @Value("${couchdb.name}") + public final void setDatabaseName(String databaseName) { + logger.info(databaseName); + this.databaseName = databaseName; + } + + /** + * This method cleans up old feedback votes at the scheduled interval. + */ + @Override + public void cleanFeedbackVotes(int cleanupFeedbackDelay) { + final long timelimitInMillis = 60000 * cleanupFeedbackDelay; + 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()); + } catch (JSONException e) { + logger.error("Could not delete Feedback document {}, error is: {} ", new Object[] {d.getId(), e}); + } + } + if (!results.isEmpty()) { + sessionService.broadcastFeedbackChanges(affectedUsers, 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) { + View view = new View("session/by_keyword"); + view.setKey(URLEncoder.encode("\"" + keyword + "\"")); + ViewResults results = this.getDatabase().view(view); + + if (results.getJSONArray("rows").optJSONObject(0) == null) + return null; + + Session result = (Session) JSONObject.toBean( + results.getJSONArray("rows").optJSONObject(0) + .optJSONObject("value"), Session.class); + + if (result.isActive() || result.getCreator().equals(this.actualUserName())) { + sessionService.addUserToSessionMap(this.actualUserName(), keyword); + return result; + } + + return null; + } + + @Override + public Session saveSession(Session session) { + + Document sessionDocument = new Document(); + sessionDocument.put("type","session"); + sessionDocument.put("name", session.getName()); + sessionDocument.put("shortName", session.getShortName()); + sessionDocument.put("keyword", sessionService.generateKeyword()); + sessionDocument.put("creator", this.actualUserName()); + sessionDocument.put("active", true); + try { + database.saveDocument(sessionDocument); + } catch (IOException e) { + return null; + } + + return this.getSession(sessionDocument.getString("keyword")); + } + + @Override + public Feedback getFeedback(String keyword) { + String sessionId = this.getSessionId(keyword); + if (sessionId == null) + return null; + + logger.info("Time: {}", this.currentTimestamp()); + + View view = new View("understanding/by_session"); + view.setGroup(true); + view.setStartKey(URLEncoder.encode("[\"" + sessionId + "\"]")); + view.setEndKey(URLEncoder.encode("[\"" + sessionId + "\",{}]")); + ViewResults results = this.getDatabase().view(view); + + logger.info("Feedback: {}", results.getJSONArray("rows")); + + int values[] = { 0, 0, 0, 0 }; + List<Integer> result = new ArrayList<Integer>(); + + try { + for (int i = 0; i <= 3; i++) { + String key = results.getJSONArray("rows").optJSONObject(i) + .optJSONArray("key").getString(1); + if (key.equals("Bitte schneller")) + values[0] = results.getJSONArray("rows").optJSONObject(i) + .getInt("value"); + if (key.equals("Kann folgen")) + values[1] = results.getJSONArray("rows").optJSONObject(i) + .getInt("value"); + if (key.equals("Zu schnell")) + values[2] = results.getJSONArray("rows").optJSONObject(i) + .getInt("value"); + if (key.equals("Nicht mehr dabei")) + values[3] = results.getJSONArray("rows").optJSONObject(i) + .getInt("value"); + } + } catch (Exception e) { + return new Feedback( + values[0], + values[1], + values[2], + values[3] + ); + } + + return new Feedback( + values[0], + values[1], + values[2], + values[3] + ); + } + + @Override + public boolean saveFeedback(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(); + List<Document> postedFeedback = findPreviousFeedback(sessionId, user); + + // 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; + } + } 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 { + this.getDatabase().saveDocument(feedback); + } catch (IOException e) { + return false; + } + + 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 null; + } + } + + @Override + @Transactional(isolation=Isolation.READ_COMMITTED) + public boolean sessionKeyAvailable(String keyword) { + View view = new View("session/by_keyword"); + ViewResults results = this.getDatabase().view(view); + + return ! results.containsKey(keyword); + } + + private String getSessionId(String keyword) { + View view = new View("session/by_keyword"); + view.setKey(URLEncoder.encode("\"" + keyword + "\"")); + ViewResults results = this.getDatabase().view(view); + + if (results.getJSONArray("rows").optJSONObject(0) == null) + return null; + + 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 null; + } catch (IOException e) { + return null; + } + return null; + } + + private String currentTimestamp() { + return Long.toString(System.currentTimeMillis()); + } + + private String actualUserName() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + User user = userService.getUser(authentication); + if(user == null) return null; + return user.getUsername(); + } + + private Database getDatabase() { + if (database == null) { + try { + com.fourspaces.couchdb.Session session = new com.fourspaces.couchdb.Session( + databaseHost, + databasePort + ); + + database = session.getDatabase(databaseName); + } catch (Exception e) { + logger.error( + "Cannot connect to CouchDB database '" + + databaseName + +"' on host '" + + databaseHost + + "' using port " + + databasePort + ); + } + } + + return database; + } +} diff --git a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java new file mode 100644 index 0000000000000000000000000000000000000000..4d41043b543089feeaac2ff8c2b65afadcec9352 --- /dev/null +++ b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 THM webMedia + * + * This file is part of ARSnova. + * + * ARSnova 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. + * + * ARSnova 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 de.thm.arsnova.dao; + +import de.thm.arsnova.entities.Feedback; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; + +public interface IDatabaseDao { + public void cleanFeedbackVotes(int cleanupFeedbackDelay); + public Session getSession(String keyword); + public Session saveSession(Session session); + public Feedback getFeedback(String keyword); + public boolean saveFeedback(String keyword, int value, User user); + public boolean sessionKeyAvailable(String keyword); +} \ No newline at end of file diff --git a/src/main/java/de/thm/arsnova/services/ISessionService.java b/src/main/java/de/thm/arsnova/services/ISessionService.java index 4e353c774f7749f6cd61993da44c66b7186d8ef5..cbcd5c85631fb76924f0e42837eb97bfee643a62 100644 --- a/src/main/java/de/thm/arsnova/services/ISessionService.java +++ b/src/main/java/de/thm/arsnova/services/ISessionService.java @@ -16,9 +16,12 @@ * 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 de.thm.arsnova.services; import java.util.List; +import java.util.Map; +import java.util.Set; import de.thm.arsnova.entities.Feedback; import de.thm.arsnova.entities.Session; @@ -31,10 +34,11 @@ public interface ISessionService { public Session getSession(String keyword); public Session saveSession(Session session); public Feedback getFeedback(String keyword); - public boolean postFeedback(String keyword, int value, User user); + public boolean saveFeedback(String keyword, int value, User user); public boolean sessionKeyAvailable(String keyword); public String generateKeyword(); public void addUserToSessionMap(String username, String keyword); public boolean isUserInSession(User user, String keyword); public List<String> getUsersInSession(String keyword); + public void broadcastFeedbackChanges(Map<String, Set<String>> affectedUsers, Set<String> allAffectedSessions); } \ No newline at end of file diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java index accb4cd24f7431f1b6c19e06270a38e431dc0fa6..4edd169b5e5bceac05261c6889c9802720a546fb 100644 --- a/src/main/java/de/thm/arsnova/services/SessionService.java +++ b/src/main/java/de/thm/arsnova/services/SessionService.java @@ -16,40 +16,24 @@ * 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 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.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import net.sf.json.JSONException; -import net.sf.json.JSONObject; - -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.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; -import com.fourspaces.couchdb.Database; -import com.fourspaces.couchdb.Document; -import com.fourspaces.couchdb.View; -import com.fourspaces.couchdb.ViewResults; - +import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.Feedback; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; @@ -58,9 +42,6 @@ import de.thm.arsnova.socket.ARSnovaSocketIOServer; @Service public class SessionService implements ISessionService { - @Autowired - IUserService userService; - @Autowired ARSnovaSocketIOServer server; @@ -70,269 +51,46 @@ public class SessionService implements ISessionService { @Value("${feedback.cleanup}") private int cleanupFeedbackDelay; - private String databaseHost; - private int databasePort; - private String databaseName; - - private Database database; - private static final ConcurrentHashMap<String, String> user2session = new ConcurrentHashMap<String, String>(); - public static final Logger logger = LoggerFactory.getLogger(SessionService.class); - - @Value("${couchdb.host}") - public final void setDatabaseHost(String databaseHost) { - logger.info(databaseHost); - this.databaseHost = databaseHost; - } - - @Value("${couchdb.port}") - public final void setDatabasePort(String databasePort) { - logger.info(databasePort); - this.databasePort = Integer.parseInt(databasePort); - } + @Autowired + IDatabaseDao databaseDao; - @Value("${couchdb.name}") - public final void setDatabaseName(String databaseName) { - logger.info(databaseName); - this.databaseName = databaseName; + public void setDatabaseDao(IDatabaseDao databaseDao) { + this.databaseDao = databaseDao; } - /** - * This method cleans up old feedback votes at the scheduled interval. - */ @Override @Scheduled(fixedDelay=5000) public void cleanFeedbackVotes() { - final long timelimitInMillis = 60000 * cleanupFeedbackDelay; - 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()); - } catch (JSONException e) { - logger.error("Could not delete Feedback document {}, error is: {} ", new Object[] {d.getId(), e}); - } - } - if (!results.isEmpty()) { - broadcastFeedbackChanges(affectedUsers, allAffectedSessions); - } + databaseDao.cleanFeedbackVotes(cleanupFeedbackDelay); } - - /** - * - * @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) { - View view = new View("session/by_keyword"); - view.setKey(URLEncoder.encode("\"" + keyword + "\"")); - ViewResults results = this.getDatabase().view(view); - - if (results.getJSONArray("rows").optJSONObject(0) == null) - return null; - - Session result = (Session) JSONObject.toBean( - results.getJSONArray("rows").optJSONObject(0) - .optJSONObject("value"), Session.class); - - if (result.isActive() || result.getCreator().equals(this.actualUserName())) { - this.addUserToSessionMap(this.actualUserName(), keyword); - return result; - } - - return null; + return databaseDao.getSession(keyword); } - + @Override public Session saveSession(Session session) { - - Document sessionDocument = new Document(); - sessionDocument.put("type","session"); - sessionDocument.put("name", session.getName()); - sessionDocument.put("shortName", session.getShortName()); - sessionDocument.put("keyword", this.generateKeyword()); - sessionDocument.put("creator", this.actualUserName()); - sessionDocument.put("active", true); - try { - database.saveDocument(sessionDocument); - } catch (IOException e) { - return null; - } - - return this.getSession(sessionDocument.getString("keyword")); + return databaseDao.saveSession(session); } @Override public Feedback getFeedback(String keyword) { - String sessionId = this.getSessionId(keyword); - if (sessionId == null) - return null; - - logger.info("Time: {}", this.currentTimestamp()); - - View view = new View("understanding/by_session"); - view.setGroup(true); - view.setStartKey(URLEncoder.encode("[\"" + sessionId + "\"]")); - view.setEndKey(URLEncoder.encode("[\"" + sessionId + "\",{}]")); - ViewResults results = this.getDatabase().view(view); - - logger.info("Feedback: {}", results.getJSONArray("rows")); - - int values[] = { 0, 0, 0, 0 }; - List<Integer> result = new ArrayList<Integer>(); - - try { - for (int i = 0; i <= 3; i++) { - String key = results.getJSONArray("rows").optJSONObject(i) - .optJSONArray("key").getString(1); - if (key.equals("Bitte schneller")) - values[0] = results.getJSONArray("rows").optJSONObject(i) - .getInt("value"); - if (key.equals("Kann folgen")) - values[1] = results.getJSONArray("rows").optJSONObject(i) - .getInt("value"); - if (key.equals("Zu schnell")) - values[2] = results.getJSONArray("rows").optJSONObject(i) - .getInt("value"); - if (key.equals("Nicht mehr dabei")) - values[3] = results.getJSONArray("rows").optJSONObject(i) - .getInt("value"); - } - } catch (Exception e) { - return new Feedback( - values[0], - values[1], - values[2], - values[3] - ); - } - - return new Feedback( - values[0], - values[1], - values[2], - values[3] - ); + return databaseDao.getFeedback(keyword); } @Override - 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(); - List<Document> postedFeedback = findPreviousFeedback(sessionId, user); - - // 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; - } - } 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 { - this.getDatabase().saveDocument(feedback); - } catch (IOException e) { - return false; - } - - 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 ""; - } + public boolean saveFeedback(String keyword, int value, User user) { + return databaseDao.saveFeedback(keyword, value, user); } @Override - @Transactional(isolation=Isolation.READ_COMMITTED) public boolean sessionKeyAvailable(String keyword) { - View view = new View("session/by_keyword"); - ViewResults results = this.getDatabase().view(view); - - return ! results.containsKey(keyword); + return databaseDao.sessionKeyAvailable(keyword); } - + @Override public boolean isUserInSession(de.thm.arsnova.entities.User user, String keyword) { if (keyword == null) return false; @@ -357,47 +115,23 @@ public class SessionService implements ISessionService { public void addUserToSessionMap(String username, String keyword) { user2session.put(username, keyword); } - - private String getSessionId(String keyword) { - View view = new View("session/by_keyword"); - view.setKey(URLEncoder.encode("\"" + keyword + "\"")); - ViewResults results = this.getDatabase().view(view); - - if (results.getJSONArray("rows").optJSONObject(0) == null) - return null; - 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(); + /** + * + * @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. + */ + public 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()); } - } catch (UnsupportedEncodingException e) { - return ""; - } catch (IOException e) { - return ""; } - return ""; - } - - private String currentTimestamp() { - return Long.toString(System.currentTimeMillis()); + this.server.reportUpdatedFeedbackForSessions(allAffectedSessions); } - private String actualUserName() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - User user = userService.getUser(authentication); - if(user == null) return null; - return user.getUsername(); - } - @Override public String generateKeyword() { final int low = 10000000; @@ -407,28 +141,4 @@ public class SessionService implements ISessionService { if (this.sessionKeyAvailable(keyword)) return keyword; return generateKeyword(); } - - private Database getDatabase() { - if (database == null) { - try { - com.fourspaces.couchdb.Session session = new com.fourspaces.couchdb.Session( - databaseHost, - databasePort - ); - - database = session.getDatabase(databaseName); - } catch (Exception e) { - logger.error( - "Cannot connect to CouchDB database '" - + databaseName - +"' on host '" - + databaseHost - + "' using port " - + databasePort - ); - } - } - - return database; - } } diff --git a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java index 8217009f896952a00427b93045d7f91381633c1b..ccf4f80ceea5aaead7e4279d1954afce620f8e89 100644 --- a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java +++ b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java @@ -77,7 +77,7 @@ public class ARSnovaSocketIOServer { if(u == null || sessionService.isUserInSession(u, data.getSessionkey()) == false) { return; } - sessionService.postFeedback(data.getSessionkey(), data.getValue(), u); + sessionService.saveFeedback(data.getSessionkey(), data.getValue(), u); /** * collect a list of users which are in the current session diff --git a/src/test/java/de/thm/arsnova/AbstractSpringContextTestBase.java b/src/test/java/de/thm/arsnova/AbstractSpringContextTestBase.java index daa554802bb5de6800a0f3bb54e14b8660bae762..651eec913758f622038fa2990d83f2036cf8fa34 100644 --- a/src/test/java/de/thm/arsnova/AbstractSpringContextTestBase.java +++ b/src/test/java/de/thm/arsnova/AbstractSpringContextTestBase.java @@ -37,8 +37,12 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -@ContextConfiguration(locations={"file:src/main/webapp/WEB-INF/arsnova-servlet.xml", "file:src/main/webapp/WEB-INF/spring/spring-main.xml"}) @RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={ + "file:src/main/webapp/WEB-INF/arsnova-servlet.xml", + "file:src/main/webapp/WEB-INF/spring/spring-main.xml", + "file:src/test/resources/test-config.xml" +}) public class AbstractSpringContextTestBase extends AbstractJUnit4SpringContextTests { protected MockHttpServletRequest request; diff --git a/src/test/java/de/thm/arsnova/controller/LoginControllerTest.java b/src/test/java/de/thm/arsnova/controller/LoginControllerTest.java index 8f771dbf72e1bdd1931916cee0bfb59341771a06..de31ecfc4ae1bbcc6ad0021d6e5c2e67424aafed 100644 --- a/src/test/java/de/thm/arsnova/controller/LoginControllerTest.java +++ b/src/test/java/de/thm/arsnova/controller/LoginControllerTest.java @@ -22,26 +22,50 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import org.junit.After; +import javax.inject.Inject; + import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; import org.springframework.web.servlet.view.RedirectView; -import de.thm.arsnova.AbstractSpringContextTestBase; +import de.thm.arsnova.LoginController; import de.thm.arsnova.entities.User; -public class LoginControllerTest extends AbstractSpringContextTestBase { +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={ + "file:src/main/webapp/WEB-INF/arsnova-servlet.xml", + "file:src/main/webapp/WEB-INF/spring/spring-main.xml", + "file:src/test/resources/test-config.xml" +}) +public class LoginControllerTest { + @Inject + private ApplicationContext applicationContext; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private HandlerAdapter handlerAdapter; + + @Autowired + private LoginController loginController; + @Before public void setUp() throws Exception { this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); + handlerAdapter = applicationContext.getBean(AnnotationMethodHandlerAdapter.class); } @Test @@ -50,7 +74,7 @@ public class LoginControllerTest extends AbstractSpringContextTestBase { request.setRequestURI("/doLogin"); request.addParameter("type", "guest"); - final ModelAndView mav = handle(request, response); + final ModelAndView mav = handlerAdapter.handle(request, response, loginController); assertNotNull(mav); assertNotNull(mav.getView()); @@ -67,7 +91,7 @@ public class LoginControllerTest extends AbstractSpringContextTestBase { request.addParameter("type", "guest"); request.addParameter("user", "Guest1234567890"); - final ModelAndView mav = handle(request, response); + final ModelAndView mav = handlerAdapter.handle(request, response, loginController); assertNotNull(mav); assertNotNull(mav.getView()); @@ -79,24 +103,25 @@ public class LoginControllerTest extends AbstractSpringContextTestBase { } + /* @Test public void testUser() throws Exception { request.setMethod("GET"); request.setRequestURI("/whoami"); - final ModelAndView mav = handle(request, response); + final ModelAndView mav = handlerAdapter.handle(request, response, loginController); assertNotNull(mav); assertTrue(mav.getModel().containsKey("user")); assertEquals(mav.getModel().get("user").getClass(), User.class); assertEquals("Guest1234567890", ((User)mav.getModel().get("user")).getUsername()); - } + }*/ @Test public void testLogoutWithoutRedirect() throws Exception { request.setMethod("GET"); request.setRequestURI("/logout"); - final ModelAndView mav = handle(request, response); + final ModelAndView mav = handlerAdapter.handle(request, response, loginController); assertNotNull(mav); assertNotNull(mav.getView()); RedirectView view = (RedirectView) mav.getView(); @@ -109,7 +134,7 @@ public class LoginControllerTest extends AbstractSpringContextTestBase { request.setRequestURI("/logout"); request.addHeader("referer", "/dojo-index.html"); - final ModelAndView mav = handle(request, response); + final ModelAndView mav = handlerAdapter.handle(request, response, loginController); assertNotNull(mav); assertNotNull(mav.getView()); RedirectView view = (RedirectView) mav.getView(); diff --git a/src/test/java/de/thm/arsnova/controller/SessionControllerTest.java b/src/test/java/de/thm/arsnova/controller/SessionControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..74804a308a04d7f568212f18d5a472283bc26ed8 --- /dev/null +++ b/src/test/java/de/thm/arsnova/controller/SessionControllerTest.java @@ -0,0 +1,60 @@ +package de.thm.arsnova.controller; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import javax.inject.Inject; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; + +import de.thm.arsnova.SessionController; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={ + "file:src/main/webapp/WEB-INF/arsnova-servlet.xml", + "file:src/main/webapp/WEB-INF/spring/spring-main.xml", + "file:src/test/resources/test-config.xml" +}) +public class SessionControllerTest { + + @Inject + private ApplicationContext applicationContext; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private HandlerAdapter handlerAdapter; + + @Autowired + private SessionController sessionController; + + @Before + public void setUp() { + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + handlerAdapter = applicationContext.getBean(AnnotationMethodHandlerAdapter.class); + } + + @Test + public void testShouldNotGetMissingSession() { + request.setMethod("GET"); + request.setRequestURI("/session/12345678"); + try { + final ModelAndView mav = handlerAdapter.handle(request, response, sessionController); + assertNull(mav); + } catch (Exception e) { + e.printStackTrace(); + fail("An exception occured"); + } + + } +} diff --git a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java new file mode 100644 index 0000000000000000000000000000000000000000..10fa394a6c1e524aca7eba8e68dd71e63245f59d --- /dev/null +++ b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java @@ -0,0 +1,98 @@ +package de.thm.arsnova.dao; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import de.thm.arsnova.entities.Feedback; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.User; + +@Component +@Scope("singleton") +public class StubDatabaseDao implements IDatabaseDao { + + private Map<String, Session> stubSessions = new ConcurrentHashMap<String, Session>(); + private Map<String, Feedback> stubFeedbacks = new ConcurrentHashMap<String, Feedback>(); + + public StubDatabaseDao() { + fillWithDummySessions(); + fillWithDummyFeedbacks(); + } + + private void fillWithDummySessions() { + Session session = new Session(); + session.setActive(true); + session.setCreator("ptsr00"); + session.setKeyword("12345678"); + session.setName("TestSession1"); + session.setShortName("TS1"); + + this.stubSessions.put("12345678", session); + + session.setActive(true); + session.setCreator("ptsr00"); + session.setKeyword("87654321"); + session.setName("TestSession2"); + session.setShortName("TS2"); + + this.stubSessions.put("87654321", session); + } + + private void fillWithDummyFeedbacks() { + stubFeedbacks.put("12345678", new Feedback(0, 0, 0, 0)); + stubFeedbacks.put("87654321", new Feedback(2, 3, 5, 7)); + } + + @Override + public void cleanFeedbackVotes(int cleanupFeedbackDelay) { + stubSessions.clear(); + } + + @Override + public Session getSession(String keyword) { + return stubSessions.get(keyword); + } + + @Override + public Session saveSession(Session session) { + stubSessions.put(session.getKeyword(), session); + return session; + } + + @Override + public Feedback getFeedback(String keyword) { + return stubFeedbacks.get(keyword); + } + + @Override + public boolean saveFeedback(String keyword, int value, User user) { + if (stubFeedbacks.get(keyword) == null) { + stubFeedbacks.put(keyword, new Feedback(0, 0, 0, 0)); + } + + Feedback sessionFeedback = stubFeedbacks.get(keyword); + + List<Integer> values = sessionFeedback.getValues(); + values.set(value, values.get(value) + 1); + + sessionFeedback = new Feedback(values.get(0), values.get(1), values.get(2), values.get(3)); + + stubFeedbacks.put( + keyword, + sessionFeedback + ); + + return true; + } + + @Override + public boolean sessionKeyAvailable(String keyword) { + System.out.println(stubSessions.get(keyword)); + return (stubSessions.get(keyword) == null); + } + +} diff --git a/src/test/java/de/thm/arsnova/services/SessionServiceTest.java b/src/test/java/de/thm/arsnova/services/SessionServiceTest.java index 9bf9798332842bdd8e90a416b38367261c5c2e19..d2b0b41a882e5f5c333df0bc64db7314ff4b4294 100644 --- a/src/test/java/de/thm/arsnova/services/SessionServiceTest.java +++ b/src/test/java/de/thm/arsnova/services/SessionServiceTest.java @@ -18,16 +18,34 @@ */ package de.thm.arsnova.services; -import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={ + "file:src/main/webapp/WEB-INF/arsnova-servlet.xml", + "file:src/main/webapp/WEB-INF/spring/spring-main.xml", + "file:src/test/resources/test-config.xml" +}) public class SessionServiceTest { + + @Autowired + ISessionService sessionService; + + @Test + public void testShouldFail() { + if (sessionService.getFeedback("00000000") != null) fail("Result is not null"); + } @Test - public void shouldGenerateSessionKeyword() { - SessionService session = new SessionService(); - System.out.println(session.generateKeyword()); - assertTrue(session.generateKeyword().matches("^[0-9]{8}$")); + public void testShouldGenerateSessionKeyword() { + System.out.println(sessionService.generateKeyword()); + assertTrue(sessionService.generateKeyword().matches("^[0-9]{8}$")); } } \ No newline at end of file diff --git a/src/test/resources/test-config.xml b/src/test/resources/test-config.xml new file mode 100644 index 0000000000000000000000000000000000000000..db94937d9b63d296ec9c3b8423b22c1903e5e80e --- /dev/null +++ b/src/test/resources/test-config.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:context="http://www.springframework.org/schema/context" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> + + <bean id="sessionService" class="de.thm.arsnova.services.SessionService"> + <property name="databaseDao" ref="databaseDao"></property> + </bean> + + <bean id="databaseDao" class="de.thm.arsnova.dao.StubDatabaseDao"> + </bean> + +</beans>