Commit 245f281f authored by Daniel Gerhardt's avatar Daniel Gerhardt

Migrate remaining CouchDB code to Ektorp

Code for the following domains has been migrated:
* VisitedSession
* MotdList
* Statistics
* LearingProgress
parent c18ffc9b
......@@ -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) {
......
......@@ -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;
}
}
......@@ -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);
}
......@@ -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;
......
......@@ -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
......
......@@ -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
......
......@@ -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) {
......
......@@ -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;
}
}
......@@ -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");
......
package de.thm.arsnova.persistance;
import de.thm.arsnova.entities.MotdList;
public interface MotdListRepository {
MotdList getMotdListForUser(final String username);
MotdList createOrUpdateMotdList(MotdList motdlist);
}
package de.thm.arsnova.persistance;
import de.thm.arsnova.domain.CourseScore;
import de.thm.arsnova.entities.Session;
public interface SessionStatisticsRepository {
CourseScore getLearningProgress(Session session);
}
package de.thm.arsnova.persistance;
import de.thm.arsnova.entities.Statistics;
public interface StatisticsRepository {
Statistics getStatistics();
}
package de.thm.arsnova.persistance;
public interface VisitedSessionRepository {
int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore);
}
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;
}
}
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