From 245f281ffd17980d832aab4e3ef75d3c475e02b6 Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <code@dgerhardt.net> Date: Sun, 2 Jul 2017 15:18:31 +0200 Subject: [PATCH] Migrate remaining CouchDB code to Ektorp Code for the following domains has been migrated: * VisitedSession * MotdList * Statistics * LearingProgress --- .../java/de/thm/arsnova/config/AppConfig.java | 44 ++-- .../java/de/thm/arsnova/dao/CouchDBDao.java | 220 ------------------ .../java/de/thm/arsnova/dao/IDatabaseDao.java | 16 -- .../domain/LearningProgressFactory.java | 8 +- .../domain/PointBasedLearningProgress.java | 6 +- .../domain/QuestionBasedLearningProgress.java | 6 +- .../domain/VariantLearningProgress.java | 10 +- .../de/thm/arsnova/entities/MotdList.java | 51 ++-- .../CouchDbTypeFieldConverter.java | 2 + .../persistance/MotdListRepository.java | 8 + .../SessionStatisticsRepository.java | 8 + .../persistance/StatisticsRepository.java | 7 + .../persistance/VisitedSessionRepository.java | 5 + .../couchdb/CouchDbMotdListRepository.java | 46 ++++ .../CouchDbSessionStatisticsRepository.java | 54 +++++ .../couchdb/CouchDbStatisticsRepository.java | 88 +++++++ .../CouchDbVisitedSessionRepository.java | 70 ++++++ .../de/thm/arsnova/services/MotdService.java | 17 +- .../thm/arsnova/services/SessionService.java | 10 +- .../arsnova/services/StatisticsService.java | 9 +- .../de/thm/arsnova/dao/StubDatabaseDao.java | 36 --- .../PointBasedLearningProgressTest.java | 4 +- .../QuestionBasedLearningProgressTest.java | 4 +- 23 files changed, 373 insertions(+), 356 deletions(-) create mode 100644 src/main/java/de/thm/arsnova/persistance/MotdListRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java create mode 100644 src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java diff --git a/src/main/java/de/thm/arsnova/config/AppConfig.java b/src/main/java/de/thm/arsnova/config/AppConfig.java index 49297579d..cd5576087 100644 --- a/src/main/java/de/thm/arsnova/config/AppConfig.java +++ b/src/main/java/de/thm/arsnova/config/AppConfig.java @@ -23,30 +23,12 @@ import com.fasterxml.jackson.databind.SerializationFeature; import de.thm.arsnova.ImageUtils; import de.thm.arsnova.connector.client.ConnectorClient; import de.thm.arsnova.connector.client.ConnectorClientImpl; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.entities.LogEntry; -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.entities.*; import de.thm.arsnova.entities.serialization.CouchDbDocumentModule; import de.thm.arsnova.entities.serialization.CouchDbObjectMapperFactory; import de.thm.arsnova.entities.serialization.View; -import de.thm.arsnova.persistance.AnswerRepository; -import de.thm.arsnova.persistance.CommentRepository; -import de.thm.arsnova.persistance.ContentRepository; -import de.thm.arsnova.persistance.LogEntryRepository; -import de.thm.arsnova.persistance.MotdRepository; -import de.thm.arsnova.persistance.SessionRepository; -import de.thm.arsnova.persistance.UserRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbAnswerRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbCommentRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbContentRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbLogEntryRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbMotdRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbSessionRepository; -import de.thm.arsnova.persistance.couchdb.CouchDbUserRepository; +import de.thm.arsnova.persistance.*; +import de.thm.arsnova.persistance.couchdb.*; import de.thm.arsnova.persistance.couchdb.InitializingCouchDbConnector; import de.thm.arsnova.socket.ARSnovaSocket; import de.thm.arsnova.socket.ARSnovaSocketIOServer; @@ -321,6 +303,26 @@ public class AppConfig extends WebMvcConfigurerAdapter { return new CouchDbUserRepository(DbUser.class, couchDbConnector(), false); } + @Bean + public VisitedSessionRepository visitedSessionRepository() throws Exception { + return new CouchDbVisitedSessionRepository(VisitedSession.class, couchDbConnector(), false); + } + + @Bean + public MotdListRepository motdListRepository() throws Exception { + return new CouchDbMotdListRepository(MotdList.class, couchDbConnector(), false); + } + + @Bean + public StatisticsRepository statisticsRepository() throws Exception { + return new CouchDbStatisticsRepository(Object.class, couchDbConnector(), false); + } + + @Bean + public SessionStatisticsRepository sessionStatisticsRepository() throws Exception { + return new CouchDbSessionStatisticsRepository(Object.class, couchDbConnector(), false); + } + @Bean(name = "connectorClient") public ConnectorClient connectorClient() { if (!connectorEnable) { diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java index 247d432cb..38aa4ee83 100644 --- a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java +++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java @@ -21,16 +21,13 @@ import com.fourspaces.couchdb.Database; import com.fourspaces.couchdb.Document; import com.fourspaces.couchdb.View; import com.fourspaces.couchdb.ViewResults; -import com.google.common.collect.Lists; import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.domain.CourseScore; import de.thm.arsnova.entities.*; import de.thm.arsnova.persistance.ContentRepository; import de.thm.arsnova.persistance.LogEntryRepository; import de.thm.arsnova.persistance.MotdRepository; import de.thm.arsnova.persistance.SessionRepository; import de.thm.arsnova.services.ISessionService; -import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.checkerframework.checker.nullness.qual.NonNull; import org.slf4j.Logger; @@ -38,18 +35,12 @@ import org.slf4j.LoggerFactory; import org.springframework.aop.framework.AopContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * Database implementation based on CouchDB. @@ -73,8 +64,6 @@ import java.util.Set; @Service("databaseDao") public class CouchDBDao implements IDatabaseDao { - private static final int BULK_PARTITION_SIZE = 500; - @Autowired private ISessionService sessionService; @@ -169,84 +158,6 @@ public class CouchDBDao implements IDatabaseDao { return results == null || results.getResults().isEmpty() || results.getJSONArray("rows").isEmpty(); } - @Cacheable("statistics") - @Override - public Statistics getStatistics() { - final Statistics stats = new Statistics(); - try { - final View statsView = new View("statistics/statistics"); - final View creatorView = new View("statistics/unique_session_creators"); - final View studentUserView = new View("statistics/active_student_users"); - statsView.setGroup(true); - creatorView.setGroup(true); - studentUserView.setGroup(true); - - final ViewResults statsResults = getDatabase().view(statsView); - final ViewResults creatorResults = getDatabase().view(creatorView); - final ViewResults studentUserResults = getDatabase().view(studentUserView); - - if (!isEmptyResults(statsResults)) { - final JSONArray rows = statsResults.getJSONArray("rows"); - for (int i = 0; i < rows.size(); i++) { - final JSONObject row = rows.getJSONObject(i); - final int value = row.getInt("value"); - switch (row.getString("key")) { - case "openSessions": - stats.setOpenSessions(stats.getOpenSessions() + value); - break; - case "closedSessions": - stats.setClosedSessions(stats.getClosedSessions() + value); - break; - case "deletedSessions": - /* Deleted sessions are not exposed separately for now. */ - stats.setClosedSessions(stats.getClosedSessions() + value); - break; - case "answers": - stats.setAnswers(stats.getAnswers() + value); - break; - case "lectureQuestions": - stats.setLectureQuestions(stats.getLectureQuestions() + value); - break; - case "preparationQuestions": - stats.setPreparationQuestions(stats.getPreparationQuestions() + value); - break; - case "interposedQuestions": - stats.setInterposedQuestions(stats.getInterposedQuestions() + value); - break; - case "conceptQuestions": - stats.setConceptQuestions(stats.getConceptQuestions() + value); - break; - case "flashcards": - stats.setFlashcards(stats.getFlashcards() + value); - break; - } - } - } - if (!isEmptyResults(creatorResults)) { - final JSONArray rows = creatorResults.getJSONArray("rows"); - Set<String> creators = new HashSet<>(); - for (int i = 0; i < rows.size(); i++) { - final JSONObject row = rows.getJSONObject(i); - creators.add(row.getString("key")); - } - stats.setCreators(creators.size()); - } - if (!isEmptyResults(studentUserResults)) { - final JSONArray rows = studentUserResults.getJSONArray("rows"); - Set<String> students = new HashSet<>(); - for (int i = 0; i < rows.size(); i++) { - final JSONObject row = rows.getJSONObject(i); - students.add(row.getString("key")); - } - stats.setActiveStudents(students.size()); - } - return stats; - } catch (final Exception e) { - logger.error("Could not retrieve session count.", e); - } - return stats; - } - /** * Adds convenience methods to CouchDB4J's view class. */ @@ -272,135 +183,4 @@ public class CouchDBDao implements IDatabaseDao { setKeys(sessionIds); } } - - @Override - public int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore) { - try { - View view = new View("logged_in/by_last_activity_for_guests"); - view.setEndKey(lastActivityBefore); - List<Document> results = this.getDatabase().view(view).getResults(); - - int count = 0; - List<List<Document>> partitions = Lists.partition(results, BULK_PARTITION_SIZE); - for (List<Document> partition: partitions) { - final List<Document> newDocs = new ArrayList<>(); - for (final Document oldDoc : partition) { - final Document newDoc = new Document(); - newDoc.setId(oldDoc.getId()); - newDoc.setRev(oldDoc.getJSONObject("value").getString("_rev")); - newDoc.put("_deleted", true); - newDocs.add(newDoc); - logger.debug("Marked logged_in document {} for deletion.", oldDoc.getId()); - /* Use log type 'user' since effectively the user is deleted in case of guests */ - dbLogger.log("delete", "type", "user", "id", oldDoc.getId()); - } - - if (!newDocs.isEmpty()) { - if (getDatabase().bulkSaveDocuments(newDocs.toArray(new Document[newDocs.size()]))) { - count += newDocs.size(); - } else { - logger.error("Could not bulk delete visited session lists."); - } - } - } - - if (count > 0) { - logger.info("Deleted {} visited session lists of inactive users.", count); - dbLogger.log("cleanup", "type", "visitedsessions", "count", count); - } - - return count; - } catch (IOException e) { - logger.error("Could not delete visited session lists of inactive users.", e); - } - - return 0; - } - - @Cacheable("learningprogress") - @Override - public CourseScore getLearningProgress(final Session session) { - final View maximumValueView = new View("learning_progress/maximum_value_of_question"); - final View answerSumView = new View("learning_progress/question_value_achieved_for_user"); - maximumValueView.setStartKeyArray(session.getId()); - maximumValueView.setEndKeyArray(session.getId(), "{}"); - answerSumView.setStartKeyArray(session.getId()); - answerSumView.setEndKeyArray(session.getId(), "{}"); - - final List<Document> maximumValueResult = getDatabase().view(maximumValueView).getResults(); - final List<Document> answerSumResult = getDatabase().view(answerSumView).getResults(); - - CourseScore courseScore = new CourseScore(); - - // no results found - if (maximumValueResult.isEmpty() && answerSumResult.isEmpty()) { - return courseScore; - } - - // collect mapping (questionId -> max value) - for (Document d : maximumValueResult) { - String questionId = d.getJSONArray("key").getString(1); - JSONObject value = d.getJSONObject("value"); - int questionScore = value.getInt("value"); - String questionVariant = value.getString("questionVariant"); - int piRound = value.getInt("piRound"); - courseScore.addQuestion(questionId, questionVariant, piRound, questionScore); - } - // collect mapping (questionId -> (user -> value)) - for (Document d : answerSumResult) { - String username = d.getJSONArray("key").getString(1); - JSONObject value = d.getJSONObject("value"); - String questionId = value.getString("questionId"); - int userscore = value.getInt("score"); - int piRound = value.getInt("piRound"); - courseScore.addAnswer(questionId, piRound, username, userscore); - } - return courseScore; - } - - @Override - @Cacheable(cacheNames = "motdlist", key = "#p0") - public MotdList getMotdListForUser(final String username) { - View view = new View("motdlist/doc_by_username"); - view.setKey(username); - - ViewResults results = this.getDatabase().view(view); - - MotdList motdlist = new MotdList(); - for (final Document d : results.getResults()) { - motdlist.set_id(d.getId()); - motdlist.set_rev(d.getJSONObject("value").getString("_rev")); - motdlist.setUsername(d.getJSONObject("value").getString("username")); - motdlist.setMotdkeys(d.getJSONObject("value").getString("motdkeys")); - } - return motdlist; - } - - @Override - @CachePut(cacheNames = "motdlist", key = "#p0.username") - public MotdList createOrUpdateMotdList(MotdList motdlist) { - try { - String id = motdlist.get_id(); - String rev = motdlist.get_rev(); - Document d = new Document(); - - if (null != id) { - d = database.getDocument(id, rev); - } - d.put("type", "motdlist"); - d.put("username", motdlist.getUsername()); - d.put("motdkeys", motdlist.getMotdkeys()); - - database.saveDocument(d, id); - motdlist.set_id(d.getId()); - motdlist.set_rev(d.getRev()); - - return motdlist; - } catch (IOException e) { - logger.error("Could not save MotD list {}.", motdlist, e); - } - - return null; - } - } diff --git a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java index 8337277a7..48729d116 100644 --- a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java +++ b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java @@ -17,25 +17,9 @@ */ package de.thm.arsnova.dao; -import de.thm.arsnova.domain.CourseScore; -import de.thm.arsnova.entities.*; - -import java.util.List; - /** * All methods the database must support. */ public interface IDatabaseDao { - - int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore); - - CourseScore getLearningProgress(Session session); - - Statistics getStatistics(); - <T> T getObjectFromId(String documentId, Class<T> klass); - - MotdList getMotdListForUser(final String username); - - MotdList createOrUpdateMotdList(MotdList motdlist); } diff --git a/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java b/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java index d03d0eb24..a5596c0b7 100644 --- a/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java +++ b/src/main/java/de/thm/arsnova/domain/LearningProgressFactory.java @@ -17,8 +17,8 @@ */ package de.thm.arsnova.domain; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.events.*; +import de.thm.arsnova.persistance.SessionStatisticsRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.context.ApplicationEventPublisher; @@ -34,7 +34,7 @@ import org.springframework.stereotype.Component; public class LearningProgressFactory implements NovaEventVisitor, ILearningProgressFactory, ApplicationEventPublisherAware { @Autowired - private IDatabaseDao databaseDao; + private SessionStatisticsRepository sessionStatisticsRepository; private ApplicationEventPublisher publisher; @@ -42,9 +42,9 @@ public class LearningProgressFactory implements NovaEventVisitor, ILearningProgr public LearningProgress create(String progressType, String questionVariant) { VariantLearningProgress learningProgress; if ("questions".equals(progressType)) { - learningProgress = new QuestionBasedLearningProgress(databaseDao); + learningProgress = new QuestionBasedLearningProgress(sessionStatisticsRepository); } else { - learningProgress = new PointBasedLearningProgress(databaseDao); + learningProgress = new PointBasedLearningProgress(sessionStatisticsRepository); } learningProgress.setQuestionVariant(questionVariant); return learningProgress; diff --git a/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java b/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java index a904caf6b..21f2fbd3d 100644 --- a/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java +++ b/src/main/java/de/thm/arsnova/domain/PointBasedLearningProgress.java @@ -17,17 +17,17 @@ */ package de.thm.arsnova.domain; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.User; import de.thm.arsnova.entities.transport.LearningProgressValues; +import de.thm.arsnova.persistance.SessionStatisticsRepository; /** * Calculates learning progress based on a question's value. */ public class PointBasedLearningProgress extends VariantLearningProgress { - public PointBasedLearningProgress(IDatabaseDao dao) { - super(dao); + public PointBasedLearningProgress(SessionStatisticsRepository sessionStatisticsRepository) { + super(sessionStatisticsRepository); } @Override diff --git a/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java b/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java index dba9b4180..37b6c6397 100644 --- a/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java +++ b/src/main/java/de/thm/arsnova/domain/QuestionBasedLearningProgress.java @@ -17,9 +17,9 @@ */ package de.thm.arsnova.domain; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.User; import de.thm.arsnova.entities.transport.LearningProgressValues; +import de.thm.arsnova.persistance.SessionStatisticsRepository; /** * Calculates learning progress based on overall correctness of an answer. A question is answered correctly if and @@ -27,8 +27,8 @@ import de.thm.arsnova.entities.transport.LearningProgressValues; */ public class QuestionBasedLearningProgress extends VariantLearningProgress { - public QuestionBasedLearningProgress(IDatabaseDao dao) { - super(dao); + public QuestionBasedLearningProgress(SessionStatisticsRepository sessionStatisticsRepository) { + super(sessionStatisticsRepository); } @Override diff --git a/src/main/java/de/thm/arsnova/domain/VariantLearningProgress.java b/src/main/java/de/thm/arsnova/domain/VariantLearningProgress.java index 708acb4b7..7e1c06970 100644 --- a/src/main/java/de/thm/arsnova/domain/VariantLearningProgress.java +++ b/src/main/java/de/thm/arsnova/domain/VariantLearningProgress.java @@ -17,10 +17,10 @@ */ package de.thm.arsnova.domain; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; import de.thm.arsnova.entities.transport.LearningProgressValues; +import de.thm.arsnova.persistance.SessionStatisticsRepository; /** * Base class for the learning progress feature that allows filtering on the question variant. @@ -31,14 +31,14 @@ abstract class VariantLearningProgress implements LearningProgress { private String questionVariant; - private final IDatabaseDao databaseDao; + private final SessionStatisticsRepository sessionStatisticsRepository; - public VariantLearningProgress(final IDatabaseDao dao) { - this.databaseDao = dao; + public VariantLearningProgress(final SessionStatisticsRepository sessionStatisticsRepository) { + this.sessionStatisticsRepository = sessionStatisticsRepository; } private void loadProgress(final Session session) { - this.courseScore = databaseDao.getLearningProgress(session); + this.courseScore = sessionStatisticsRepository.getLearningProgress(session); } public void setQuestionVariant(final String variant) { diff --git a/src/main/java/de/thm/arsnova/entities/MotdList.java b/src/main/java/de/thm/arsnova/entities/MotdList.java index 307fb6696..35b611fee 100644 --- a/src/main/java/de/thm/arsnova/entities/MotdList.java +++ b/src/main/java/de/thm/arsnova/entities/MotdList.java @@ -17,6 +17,8 @@ */ package de.thm.arsnova.entities; +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.entities.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -24,45 +26,52 @@ import io.swagger.annotations.ApiModelProperty; * This class represents a list of motdkeys for a user. */ @ApiModel(value = "motdlist", description = "the motdlist to save the messages a user has confirmed to be read") -public class MotdList { - +public class MotdList implements Entity { + private String id; + private String rev; private String motdkeys; private String username; - private String _id; - private String _rev; + + @ApiModelProperty(required = true, value = "the couchDB ID") + @JsonView({View.Persistence.class, View.Public.class}) + public String getId() { + return id; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setId(final String id) { + this.id = id; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRevision(final String rev) { + this.rev = rev; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRevision() { + return rev; + } @ApiModelProperty(required = true, value = "the motdkeylist") + @JsonView({View.Persistence.class, View.Public.class}) public String getMotdkeys() { return motdkeys; } + @JsonView({View.Persistence.class, View.Public.class}) public void setMotdkeys(String motds) { motdkeys = motds; } @ApiModelProperty(required = true, value = "the username") + @JsonView({View.Persistence.class, View.Public.class}) public String getUsername() { return username; } + @JsonView({View.Persistence.class, View.Public.class}) public void setUsername(final String u) { username = u; } - - @ApiModelProperty(required = true, value = "the couchDB ID") - public String get_id() { - return _id; - } - - public void set_id(final String id) { - _id = id; - } - - public void set_rev(final String rev) { - _rev = rev; - } - - public String get_rev() { - return _rev; - } } diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java b/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java index 3a3c6c1eb..5b2199fef 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java +++ b/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java @@ -27,6 +27,7 @@ import de.thm.arsnova.entities.Entity; import de.thm.arsnova.entities.LogEntry; import de.thm.arsnova.entities.Motd; import de.thm.arsnova.entities.Content; +import de.thm.arsnova.entities.MotdList; import de.thm.arsnova.entities.Session; import java.util.HashMap; @@ -39,6 +40,7 @@ public class CouchDbTypeFieldConverter implements Converter<Class<? extends Enti typeMapping.put(LogEntry.class, "log"); typeMapping.put(DbUser.class, "userdetails"); typeMapping.put(Motd.class, "motd"); + typeMapping.put(MotdList.class, "motdlist"); typeMapping.put(Session.class, "session"); typeMapping.put(Comment.class, "interposed_question"); typeMapping.put(Content.class, "skill_question"); diff --git a/src/main/java/de/thm/arsnova/persistance/MotdListRepository.java b/src/main/java/de/thm/arsnova/persistance/MotdListRepository.java new file mode 100644 index 000000000..a044fe7ed --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/MotdListRepository.java @@ -0,0 +1,8 @@ +package de.thm.arsnova.persistance; + +import de.thm.arsnova.entities.MotdList; + +public interface MotdListRepository { + MotdList getMotdListForUser(final String username); + MotdList createOrUpdateMotdList(MotdList motdlist); +} diff --git a/src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java new file mode 100644 index 000000000..193da0333 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java @@ -0,0 +1,8 @@ +package de.thm.arsnova.persistance; + +import de.thm.arsnova.domain.CourseScore; +import de.thm.arsnova.entities.Session; + +public interface SessionStatisticsRepository { + CourseScore getLearningProgress(Session session); +} diff --git a/src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java new file mode 100644 index 000000000..ea334f0d6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java @@ -0,0 +1,7 @@ +package de.thm.arsnova.persistance; + +import de.thm.arsnova.entities.Statistics; + +public interface StatisticsRepository { + Statistics getStatistics(); +} diff --git a/src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java new file mode 100644 index 000000000..ce0b23581 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java @@ -0,0 +1,5 @@ +package de.thm.arsnova.persistance; + +public interface VisitedSessionRepository { + int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore); +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java new file mode 100644 index 000000000..05bef30ab --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java @@ -0,0 +1,46 @@ +package de.thm.arsnova.persistance.couchdb; + +import de.thm.arsnova.entities.MotdList; +import de.thm.arsnova.persistance.MotdListRepository; +import org.ektorp.CouchDbConnector; +import org.ektorp.DbAccessException; +import org.ektorp.support.CouchDbRepositorySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; + +import java.util.List; + +public class CouchDbMotdListRepository extends CouchDbRepositorySupport<MotdList> implements MotdListRepository { + private static final Logger logger = LoggerFactory.getLogger(CouchDbMotdListRepository.class); + + public CouchDbMotdListRepository(Class<MotdList> type, CouchDbConnector db, boolean createIfNotExists) { + super(type, db, createIfNotExists); + } + + @Override + @Cacheable(cacheNames = "motdlist", key = "#p0") + public MotdList getMotdListForUser(final String username) { + List<MotdList> motdListList = queryView("by_username", username); + return motdListList.isEmpty() ? new MotdList() : motdListList.get(0); + } + + @Override + @CachePut(cacheNames = "motdlist", key = "#p0.username") + public MotdList createOrUpdateMotdList(MotdList motdlist) { + try { + if (motdlist.getId() != null) { + update(motdlist); + } else { + db.create(motdlist); + } + + return motdlist; + } catch (DbAccessException e) { + logger.error("Could not save MotD list {}.", motdlist, e); + } + + return null; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java new file mode 100644 index 000000000..be48429e2 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java @@ -0,0 +1,54 @@ +package de.thm.arsnova.persistance.couchdb; + +import com.fasterxml.jackson.databind.JsonNode; +import de.thm.arsnova.domain.CourseScore; +import de.thm.arsnova.entities.Session; +import de.thm.arsnova.persistance.SessionStatisticsRepository; +import org.ektorp.ComplexKey; +import org.ektorp.CouchDbConnector; +import org.ektorp.ViewResult; +import org.ektorp.support.CouchDbRepositorySupport; +import org.springframework.cache.annotation.Cacheable; + +public class CouchDbSessionStatisticsRepository extends CouchDbRepositorySupport implements SessionStatisticsRepository { + public CouchDbSessionStatisticsRepository(Class type, CouchDbConnector db, boolean createIfNotExists) { + super(type, db, "learning_progress", createIfNotExists); + } + + @Cacheable("learningprogress") + @Override + public CourseScore getLearningProgress(final Session session) { + final ViewResult maximumValueResult = db.queryView(createQuery("maximum_value_of_question") + .startKey(ComplexKey.of(session.getId())) + .endKey(ComplexKey.of(session.getId(), ComplexKey.emptyObject()))); + final ViewResult answerSumResult = db.queryView(createQuery("question_value_achieved_for_user") + .startKey(ComplexKey.of(session.getId())) + .endKey(ComplexKey.of(session.getId(), ComplexKey.emptyObject()))); + final CourseScore courseScore = new CourseScore(); + + // no results found + if (maximumValueResult.isEmpty() && answerSumResult.isEmpty()) { + return courseScore; + } + + // collect mapping (questionId -> max value) + for (ViewResult.Row row : maximumValueResult) { + final String questionId = row.getKeyAsNode().get(1).asText(); + final JsonNode value = row.getValueAsNode(); + final int questionScore = value.get("value").asInt(); + final String questionVariant = value.get("questionVariant").asText(); + final int piRound = value.get("piRound").asInt(); + courseScore.addQuestion(questionId, questionVariant, piRound, questionScore); + } + // collect mapping (questionId -> (user -> value)) + for (ViewResult.Row row : answerSumResult) { + final String username = row.getKeyAsNode().get(1).asText(); + final JsonNode value = row.getValueAsNode(); + final String questionId = value.get("questionId").asText(); + final int userscore = value.get("score").asInt(); + final int piRound = value.get("piRound").asInt(); + courseScore.addAnswer(questionId, piRound, username, userscore); + } + return courseScore; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java new file mode 100644 index 000000000..20ed90676 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java @@ -0,0 +1,88 @@ +package de.thm.arsnova.persistance.couchdb; + +import de.thm.arsnova.entities.Statistics; +import de.thm.arsnova.persistance.StatisticsRepository; +import org.ektorp.CouchDbConnector; +import org.ektorp.DbAccessException; +import org.ektorp.ViewResult; +import org.ektorp.support.CouchDbRepositorySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.annotation.Cacheable; + +import java.util.HashSet; +import java.util.Set; + +public class CouchDbStatisticsRepository extends CouchDbRepositorySupport implements StatisticsRepository { + private static final Logger logger = LoggerFactory.getLogger(CouchDbStatisticsRepository.class); + + public CouchDbStatisticsRepository(Class type, CouchDbConnector db, boolean createIfNotExists) { + super(type, db, "statistics", createIfNotExists); + } + + @Cacheable("statistics") + @Override + public Statistics getStatistics() { + final Statistics stats = new Statistics(); + try { + final ViewResult statsResult = db.queryView(createQuery("statistics").group(true)); + final ViewResult creatorResult = db.queryView(createQuery("unique_session_creators").group(true)); + final ViewResult studentUserResult = db.queryView(createQuery("active_student_users").group(true)); + + if (!statsResult.isEmpty()) { + for (ViewResult.Row row: statsResult.getRows()) { + final int value = row.getValueAsInt(); + switch (row.getKey()) { + case "openSessions": + stats.setOpenSessions(stats.getOpenSessions() + value); + break; + case "closedSessions": + stats.setClosedSessions(stats.getClosedSessions() + value); + break; + case "deletedSessions": + /* Deleted sessions are not exposed separately for now. */ + stats.setClosedSessions(stats.getClosedSessions() + value); + break; + case "answers": + stats.setAnswers(stats.getAnswers() + value); + break; + case "lectureQuestions": + stats.setLectureQuestions(stats.getLectureQuestions() + value); + break; + case "preparationQuestions": + stats.setPreparationQuestions(stats.getPreparationQuestions() + value); + break; + case "interposedQuestions": + stats.setInterposedQuestions(stats.getInterposedQuestions() + value); + break; + case "conceptQuestions": + stats.setConceptQuestions(stats.getConceptQuestions() + value); + break; + case "flashcards": + stats.setFlashcards(stats.getFlashcards() + value); + break; + } + } + } + if (!creatorResult.isEmpty()) { + Set<String> creators = new HashSet<>(); + for (ViewResult.Row row: statsResult.getRows()) { + creators.add(row.getKey()); + } + stats.setCreators(creators.size()); + } + if (!studentUserResult.isEmpty()) { + Set<String> students = new HashSet<>(); + for (ViewResult.Row row: statsResult.getRows()) { + students.add(row.getKey()); + } + stats.setActiveStudents(students.size()); + } + return stats; + } catch (final DbAccessException e) { + logger.error("Could not retrieve statistics.", e); + } + + return stats; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java new file mode 100644 index 000000000..e682387a7 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java @@ -0,0 +1,70 @@ +package de.thm.arsnova.persistance.couchdb; + +import com.google.common.collect.Lists; +import de.thm.arsnova.entities.VisitedSession; +import de.thm.arsnova.persistance.LogEntryRepository; +import de.thm.arsnova.persistance.VisitedSessionRepository; +import org.ektorp.BulkDeleteDocument; +import org.ektorp.CouchDbConnector; +import org.ektorp.DbAccessException; +import org.ektorp.DocumentOperationResult; +import org.ektorp.ViewResult; +import org.ektorp.support.CouchDbRepositorySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +public class CouchDbVisitedSessionRepository extends CouchDbRepositorySupport<VisitedSession> implements VisitedSessionRepository { + private static final int BULK_PARTITION_SIZE = 500; + + private static final Logger logger = LoggerFactory.getLogger(CouchDbVisitedSessionRepository.class); + + @Autowired + private LogEntryRepository dbLogger; + + public CouchDbVisitedSessionRepository(Class<VisitedSession> type, CouchDbConnector db, boolean createIfNotExists) { + super(type, db, createIfNotExists); + } + + @Override + public int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore) { + try { + ViewResult result = db.queryView(createQuery("by_last_activity_for_guests").endKey(lastActivityBefore)); + + int count = 0; + List<List<ViewResult.Row>> partitions = Lists.partition(result.getRows(), BULK_PARTITION_SIZE); + for (List<ViewResult.Row> partition: partitions) { + final List<BulkDeleteDocument> newDocs = new ArrayList<>(); + for (final ViewResult.Row oldDoc : partition) { + final BulkDeleteDocument newDoc = new BulkDeleteDocument(oldDoc.getId(), oldDoc.getValueAsNode().get("_rev").asText()); + newDocs.add(newDoc); + logger.debug("Marked logged_in document {} for deletion.", oldDoc.getId()); + /* Use log type 'user' since effectively the user is deleted in case of guests */ + dbLogger.log("delete", "type", "user", "id", oldDoc.getId()); + } + + if (!newDocs.isEmpty()) { + List<DocumentOperationResult> results = db.executeBulk(newDocs); + count += newDocs.size() - results.size(); + if (!results.isEmpty()) { + logger.error("Could not bulk delete some visited session lists."); + } + } + } + + if (count > 0) { + logger.info("Deleted {} visited session lists of inactive users.", count); + dbLogger.log("cleanup", "type", "visitedsessions", "count", count); + } + + return count; + } catch (DbAccessException e) { + logger.error("Could not delete visited session lists of inactive users.", e); + } + + return 0; + } +} diff --git a/src/main/java/de/thm/arsnova/services/MotdService.java b/src/main/java/de/thm/arsnova/services/MotdService.java index 4df366c73..674525ec2 100644 --- a/src/main/java/de/thm/arsnova/services/MotdService.java +++ b/src/main/java/de/thm/arsnova/services/MotdService.java @@ -17,12 +17,12 @@ */ package de.thm.arsnova.services; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.Motd; import de.thm.arsnova.entities.MotdList; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; import de.thm.arsnova.exceptions.BadRequestException; +import de.thm.arsnova.persistance.MotdListRepository; import de.thm.arsnova.persistance.MotdRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; @@ -38,10 +38,6 @@ import java.util.StringTokenizer; */ @Service public class MotdService implements IMotdService { - - @Autowired - private IDatabaseDao databaseDao; - @Autowired private IUserService userService; @@ -51,9 +47,8 @@ public class MotdService implements IMotdService { @Autowired private MotdRepository motdRepository; - public void setDatabaseDao(final IDatabaseDao databaseDao) { - this.databaseDao = databaseDao; - } + @Autowired + private MotdListRepository motdListRepository; @Override @PreAuthorize("isAuthenticated()") @@ -175,7 +170,7 @@ public class MotdService implements IMotdService { public MotdList getMotdListForUser(final String username) { final User user = userService.getCurrentUser(); if (username.equals(user.getUsername()) && !"guest".equals(user.getType())) { - return databaseDao.getMotdListForUser(username); + return motdListRepository.getMotdListForUser(username); } return null; } @@ -185,7 +180,7 @@ public class MotdService implements IMotdService { public MotdList saveUserMotdList(MotdList motdList) { final User user = userService.getCurrentUser(); if (user.getUsername().equals(motdList.getUsername())) { - return databaseDao.createOrUpdateMotdList(motdList); + return motdListRepository.createOrUpdateMotdList(motdList); } return null; } @@ -195,7 +190,7 @@ public class MotdService implements IMotdService { public MotdList updateUserMotdList(MotdList motdList) { final User user = userService.getCurrentUser(); if (user.getUsername().equals(motdList.getUsername())) { - return databaseDao.createOrUpdateMotdList(motdList); + return motdListRepository.createOrUpdateMotdList(motdList); } return null; } diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java index 97b522b42..0257be71d 100644 --- a/src/main/java/de/thm/arsnova/services/SessionService.java +++ b/src/main/java/de/thm/arsnova/services/SessionService.java @@ -20,7 +20,6 @@ package de.thm.arsnova.services; import de.thm.arsnova.ImageUtils; import de.thm.arsnova.connector.client.ConnectorClient; import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.domain.ILearningProgressFactory; import de.thm.arsnova.domain.LearningProgress; import de.thm.arsnova.entities.LearningProgressOptions; @@ -42,6 +41,7 @@ import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.exceptions.PayloadTooLargeException; import de.thm.arsnova.exceptions.UnauthorizedException; import de.thm.arsnova.persistance.SessionRepository; +import de.thm.arsnova.persistance.VisitedSessionRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -105,7 +105,7 @@ public class SessionService implements ISessionService, ApplicationEventPublishe private static final long SESSION_INACTIVITY_CHECK_INTERVAL_MS = 30 * 60 * 1000L; @Autowired - private IDatabaseDao databaseDao; + private VisitedSessionRepository visitedSessionRepository; @Autowired private IUserService userService; @@ -148,14 +148,10 @@ public class SessionService implements ISessionService, ApplicationEventPublishe logger.info("Delete lists of visited session for inactive users."); long unixTime = System.currentTimeMillis(); long lastActivityBefore = unixTime - guestSessionInactivityThresholdDays * 24 * 60 * 60 * 1000L; - databaseDao.deleteInactiveGuestVisitedSessionLists(lastActivityBefore); + visitedSessionRepository.deleteInactiveGuestVisitedSessionLists(lastActivityBefore); } } - public void setDatabaseDao(final IDatabaseDao newDatabaseDao) { - databaseDao = newDatabaseDao; - } - @Override public Session joinSession(final String keyword, final UUID socketId) { /* Socket.IO solution */ diff --git a/src/main/java/de/thm/arsnova/services/StatisticsService.java b/src/main/java/de/thm/arsnova/services/StatisticsService.java index e8e7fff3e..ddf091b72 100644 --- a/src/main/java/de/thm/arsnova/services/StatisticsService.java +++ b/src/main/java/de/thm/arsnova/services/StatisticsService.java @@ -17,8 +17,8 @@ */ package de.thm.arsnova.services; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.Statistics; +import de.thm.arsnova.persistance.StatisticsRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -29,18 +29,17 @@ import org.springframework.stereotype.Service; */ @Service public class StatisticsService implements IStatisticsService { - @Autowired - private IDatabaseDao databaseDao; + private StatisticsRepository statisticsRepository; @Autowired private IUserService userService; private Statistics statistics = new Statistics(); - @Scheduled(initialDelay = 0, fixedRate = 300000) + @Scheduled(initialDelay = 0, fixedRate = 10000) private void refreshStatistics() { - statistics = databaseDao.getStatistics(); + statistics = statisticsRepository.getStatistics(); } @Override diff --git a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java index 099e1dfd1..1901a6cc3 100644 --- a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java +++ b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java @@ -17,7 +17,6 @@ */ package de.thm.arsnova.dao; -import de.thm.arsnova.domain.CourseScore; import de.thm.arsnova.entities.*; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -96,44 +95,9 @@ public class StubDatabaseDao implements IDatabaseDao { stubQuestions.put("12345678", contents); } - @Override - public int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore) { - // TODO Auto-generated method stub - return 0; - } - - @Override - public CourseScore getLearningProgress(Session session) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Statistics getStatistics() { - final Statistics stats = new Statistics(); - stats.setOpenSessions(3); - stats.setClosedSessions(0); - stats.setLectureQuestions(0); - stats.setAnswers(0); - stats.setInterposedQuestions(0); - return stats; - } - @Override public <T> T getObjectFromId(String documentId, Class<T> klass) { // TODO Auto-generated method stub return null; } - - @Override - public MotdList getMotdListForUser(final String username) { - // TODO Auto-generated method stub - return null; - } - - @Override - public MotdList createOrUpdateMotdList(MotdList motdlist) { - // TODO Auto-generated method stub - return null; - } } diff --git a/src/test/java/de/thm/arsnova/domain/PointBasedLearningProgressTest.java b/src/test/java/de/thm/arsnova/domain/PointBasedLearningProgressTest.java index bfad95b36..6f42527dc 100644 --- a/src/test/java/de/thm/arsnova/domain/PointBasedLearningProgressTest.java +++ b/src/test/java/de/thm/arsnova/domain/PointBasedLearningProgressTest.java @@ -17,10 +17,10 @@ */ package de.thm.arsnova.domain; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.TestUser; import de.thm.arsnova.entities.User; import de.thm.arsnova.entities.transport.LearningProgressValues; +import de.thm.arsnova.persistance.SessionStatisticsRepository; import org.junit.Before; import org.junit.Test; @@ -50,7 +50,7 @@ public class PointBasedLearningProgressTest { @Before public void setUp() { this.courseScore = new CourseScore(); - IDatabaseDao db = mock(IDatabaseDao.class); + SessionStatisticsRepository db = mock(SessionStatisticsRepository.class); when(db.getLearningProgress(null)).thenReturn(courseScore); this.lp = new PointBasedLearningProgress(db); } diff --git a/src/test/java/de/thm/arsnova/domain/QuestionBasedLearningProgressTest.java b/src/test/java/de/thm/arsnova/domain/QuestionBasedLearningProgressTest.java index 7e029a6cb..8adf79257 100644 --- a/src/test/java/de/thm/arsnova/domain/QuestionBasedLearningProgressTest.java +++ b/src/test/java/de/thm/arsnova/domain/QuestionBasedLearningProgressTest.java @@ -17,10 +17,10 @@ */ package de.thm.arsnova.domain; -import de.thm.arsnova.dao.IDatabaseDao; import de.thm.arsnova.entities.TestUser; import de.thm.arsnova.entities.User; import de.thm.arsnova.entities.transport.LearningProgressValues; +import de.thm.arsnova.persistance.SessionStatisticsRepository; import org.junit.Before; import org.junit.Test; @@ -50,7 +50,7 @@ public class QuestionBasedLearningProgressTest { @Before public void setUp() { this.courseScore = new CourseScore(); - IDatabaseDao db = mock(IDatabaseDao.class); + SessionStatisticsRepository db = mock(SessionStatisticsRepository.class); when(db.getLearningProgress(null)).thenReturn(courseScore); this.lp = new QuestionBasedLearningProgress(db); } -- GitLab