Commit 0ae62670 authored by Daniel Gerhardt's avatar Daniel Gerhardt

Merge branch 'db-ektorp' into 'master'

Migration to Ektorp and refactoring of the database layer

A summary of the changes:
* CouchDB4J has been replaced by Ektorp.
* CouchDBDao has been split up into multiple repositories: One for each
  entity.
* Jackson is used instead of JSON-lib for POJO <-> JSON
  (de)serialization.
    * Jackson's `@JsonView`'s are used to mark which properties are
      (de)serialized for the API and/or CouchDB
    * A lot of persistence code could be simplified since serialization
      is handled automatically by the `ObjectMapper`.
* Entities and related Classes have been renamed:
    * `Content` <- `Question` / `SkillQuestion` / `LecturerQuestion`
    * `Comment` <- `FeedbackQuestion` / `AudienceQuestion`
* CouchDB design docs can now be automatically created by the backend if
  necessary. They are included as JavaScript code which is parsed and
  transformed to JSON at startup using Java's `ScriptEngine`.
* `MediaType`s for API versioning have been introduced:
    * `vnd.de.thm.arsnova.v2+json`: the format as used in ARSnova 2.x
    * `vnd.de.thm.arsnova.v3+json`:
        * ISO 8601 dates instead of timestamps
        * `id` instead of `_id`, `revision` instead of `_rev`
* `api.indent-response-body` can be set in configuration for pretty
  responses for API debugging.
* Empty (e.g. `null`) entity properties are ignored for serialization

See merge request !68
parents d47c41ff d54b3263
......@@ -121,6 +121,12 @@
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<!-- While commons-logging is not a required dependency, AJC fails without it. -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
......@@ -186,9 +192,14 @@
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>de.thm.couchdb4j</groupId>
<artifactId>couchdb4j</artifactId>
<version>0.8-SNAPSHOT</version>
<groupId>org.ektorp</groupId>
<artifactId>org.ektorp</artifactId>
<version>1.4.4</version>
</dependency>
<dependency>
<groupId>org.ektorp</groupId>
<artifactId>org.ektorp.spring</artifactId>
<version>1.4.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
......
......@@ -30,11 +30,11 @@ public class CacheBuster implements ICacheBuster, NovaEventVisitor {
@CacheEvict(value = "statistics", allEntries = true)
@Override
public void visit(NewInterposedQuestionEvent event) { }
public void visit(NewCommentEvent event) { }
@CacheEvict(value = "statistics", allEntries = true)
@Override
public void visit(DeleteInterposedQuestionEvent event) { }
public void visit(DeleteCommentEvent event) { }
@Override
public void visit(NewQuestionEvent event) { }
......@@ -51,7 +51,7 @@ public class CacheBuster implements ICacheBuster, NovaEventVisitor {
@Override
public void visit(LockQuestionsEvent lockQuestionsEvent) { }
@CacheEvict(value = "answers", key = "#event.Question")
@CacheEvict(value = "answers", key = "#event.content")
@Override
public void visit(NewAnswerEvent event) { }
......
......@@ -18,16 +18,27 @@
package de.thm.arsnova.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
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.*;
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.*;
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;
import de.thm.arsnova.web.CacheControlInterceptorHandler;
import de.thm.arsnova.web.CorsFilter;
import de.thm.arsnova.web.DeprecatedApiInterceptorHandler;
import de.thm.arsnova.web.ResponseInterceptorHandler;
import org.ektorp.CouchDbConnector;
import org.ektorp.impl.StdCouchDbInstance;
import org.ektorp.spring.HttpClientFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
......@@ -60,6 +71,7 @@ import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
......@@ -114,8 +126,8 @@ public class AppConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(stringMessageConverter());
converters.add(defaultJsonMessageConverter());
converters.add(apiV2JsonMessageConverter());
converters.add(defaultJsonMessageConverter());
//converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
......@@ -163,6 +175,8 @@ public class AppConfig extends WebMvcConfigurerAdapter {
@Bean
public StringHttpMessageConverter stringMessageConverter() {
StringHttpMessageConverter messageConverter = new StringHttpMessageConverter();
messageConverter.setDefaultCharset(Charset.forName("UTF-8"));
messageConverter.setWriteAcceptCharset(false);
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.TEXT_PLAIN);
messageConverter.setSupportedMediaTypes(mediaTypes);
......@@ -178,7 +192,9 @@ public class AppConfig extends WebMvcConfigurerAdapter {
.defaultViewInclusion(false)
.indentOutput(apiIndent)
.simpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(builder.build());
ObjectMapper mapper = builder.build();
mapper.setConfig(mapper.getSerializationConfig().withView(View.Public.class));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper);
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(API_V3_MEDIA_TYPE);
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
......@@ -195,8 +211,11 @@ public class AppConfig extends WebMvcConfigurerAdapter {
.defaultViewInclusion(false)
.indentOutput(apiIndent)
.featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(builder.build());
.featuresToEnable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
.modules(new CouchDbDocumentModule());
ObjectMapper mapper = builder.build();
mapper.setConfig(mapper.getSerializationConfig().withView(View.Public.class));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper);
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(API_V2_MEDIA_TYPE);
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
......
package de.thm.arsnova.config;
import de.thm.arsnova.entities.Answer;
import de.thm.arsnova.entities.Comment;
import de.thm.arsnova.entities.Content;
import de.thm.arsnova.entities.DbUser;
import de.thm.arsnova.entities.LogEntry;
import de.thm.arsnova.entities.Motd;
import de.thm.arsnova.entities.MotdList;
import de.thm.arsnova.entities.Session;
import de.thm.arsnova.entities.VisitedSession;
import de.thm.arsnova.entities.serialization.CouchDbObjectMapperFactory;
import de.thm.arsnova.persistance.*;
import de.thm.arsnova.persistance.couchdb.*;
import org.ektorp.CouchDbConnector;
import org.ektorp.impl.StdCouchDbInstance;
import org.ektorp.spring.HttpClientFactoryBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("!test")
public class PersistanceConfig {
@Value("${couchdb.name}") private String couchDbName;
@Value("${couchdb.host}") private String couchDbHost;
@Value("${couchdb.port}") private int couchDbPort;
@Bean
public CouchDbConnector couchDbConnector() throws Exception {
return new InitializingCouchDbConnector(couchDbName, couchDbInstance(), new CouchDbObjectMapperFactory());
}
@Bean
public StdCouchDbInstance couchDbInstance() throws Exception {
return new StdCouchDbInstance(couchDbHttpClientFactory().getObject());
}
@Bean
public HttpClientFactoryBean couchDbHttpClientFactory() throws Exception {
final HttpClientFactoryBean factory = new HttpClientFactoryBean();
factory.setHost(couchDbHost);
factory.setPort(couchDbPort);
return factory;
}
@Bean
public LogEntryRepository logEntryRepository() throws Exception {
return new CouchDbLogEntryRepository(LogEntry.class, couchDbConnector(), false);
}
@Bean
public UserRepository userRepository() throws Exception {
return new CouchDbUserRepository(DbUser.class, couchDbConnector(), false);
}
@Bean
public SessionRepository sessionRepository() throws Exception {
return new CouchDbSessionRepository(Session.class, couchDbConnector(), false);
}
@Bean
public CommentRepository commentRepository() throws Exception {
return new CouchDbCommentRepository(Comment.class, couchDbConnector(), false);
}
@Bean
public ContentRepository contentRepository() throws Exception {
return new CouchDbContentRepository(Content.class, couchDbConnector(), false);
}
@Bean
public AnswerRepository answerRepository() throws Exception {
return new CouchDbAnswerRepository(Answer.class, couchDbConnector(), false);
}
@Bean
public MotdRepository motdRepository() throws Exception {
return new CouchDbMotdRepository(Motd.class, couchDbConnector(), false);
}
@Bean
public MotdListRepository motdListRepository() throws Exception {
return new CouchDbMotdListRepository(MotdList.class, couchDbConnector(), false);
}
@Bean
public VisitedSessionRepository visitedSessionRepository() throws Exception {
return new CouchDbVisitedSessionRepository(VisitedSession.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);
}
}
......@@ -17,10 +17,10 @@
*/
package de.thm.arsnova.controller;
import de.thm.arsnova.entities.InterposedReadingCount;
import de.thm.arsnova.entities.transport.InterposedQuestion;
import de.thm.arsnova.entities.CommentReadingCount;
import de.thm.arsnova.entities.transport.Comment;
import de.thm.arsnova.exceptions.BadRequestException;
import de.thm.arsnova.services.IQuestionService;
import de.thm.arsnova.services.IContentService;
import de.thm.arsnova.web.DeprecatedApi;
import de.thm.arsnova.web.Pagination;
import io.swagger.annotations.Api;
......@@ -41,50 +41,50 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Handles requests related to audience questions, which are also called interposed or feedback questions.
* Handles requests related to comments.
*/
@RestController
@RequestMapping("/audiencequestion")
@Api(value = "/audiencequestion", description = "the Audience Question API")
public class AudienceQuestionController extends PaginationController {
@Api(value = "/audiencequestion", description = "the Audience Content API")
public class CommentController extends PaginationController {
@Autowired
private IQuestionService questionService;
private IContentService contentService;
@ApiOperation(value = "Count all the questions in current session",
@ApiOperation(value = "Count all the comments in current session",
nickname = "getAudienceQuestionCount")
@RequestMapping(value = "/count", method = RequestMethod.GET)
@DeprecatedApi
@Deprecated
public int getInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) {
return questionService.getInterposedCount(sessionkey);
return contentService.getInterposedCount(sessionkey);
}
@ApiOperation(value = "count all unread interposed questions",
@ApiOperation(value = "count all unread comments",
nickname = "getUnreadInterposedCount")
@RequestMapping(value = "/readcount", method = RequestMethod.GET)
@DeprecatedApi
@Deprecated
public InterposedReadingCount getUnreadInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam("sessionkey") final String sessionkey, String user) {
return questionService.getInterposedReadingCount(sessionkey, user);
public CommentReadingCount getUnreadInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam("sessionkey") final String sessionkey, String user) {
return contentService.getInterposedReadingCount(sessionkey, user);
}
@ApiOperation(value = "Retrieves all Interposed Questions for a Session",
@ApiOperation(value = "Retrieves all Comments for a Session",
nickname = "getInterposedQuestions")
@RequestMapping(value = "/", method = RequestMethod.GET)
@Pagination
public List<InterposedQuestion> getInterposedQuestions(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) {
return InterposedQuestion.fromList(questionService.getInterposedQuestions(sessionkey, offset, limit));
public List<Comment> getInterposedQuestions(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) {
return Comment.fromList(contentService.getInterposedQuestions(sessionkey, offset, limit));
}
@ApiOperation(value = "Retrieves an InterposedQuestion",
@ApiOperation(value = "Retrieves an Comment",
nickname = "getInterposedQuestion")
@RequestMapping(value = "/{questionId}", method = RequestMethod.GET)
public InterposedQuestion getInterposedQuestion(@ApiParam(value = "ID of the question that needs to be deleted", required = true) @PathVariable final String questionId) {
return new InterposedQuestion(questionService.readInterposedQuestion(questionId));
public Comment getInterposedQuestion(@ApiParam(value = "ID of the Comment that needs to be deleted", required = true) @PathVariable final String questionId) {
return new Comment(contentService.readInterposedQuestion(questionId));
}
@ApiOperation(value = "Creates a new Interposed Question for a Session and returns the InterposedQuestion's data",
@ApiOperation(value = "Creates a new Comment for a Session and returns the Comment's data",
nickname = "postInterposedQuestion")
@ApiResponses(value = {
@ApiResponse(code = 400, message = HTML_STATUS_400)
......@@ -93,19 +93,19 @@ public class AudienceQuestionController extends PaginationController {
@ResponseStatus(HttpStatus.CREATED)
public void postInterposedQuestion(
@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey,
@ApiParam(value = "the body from the new question", required = true) @RequestBody final de.thm.arsnova.entities.InterposedQuestion question
@ApiParam(value = "the body from the new comment", required = true) @RequestBody final de.thm.arsnova.entities.Comment comment
) {
if (questionService.saveQuestion(question)) {
if (contentService.saveQuestion(comment)) {
return;
}
throw new BadRequestException();
}
@ApiOperation(value = "Deletes an InterposedQuestion",
@ApiOperation(value = "Deletes a Comment",
nickname = "deleteInterposedQuestion")
@RequestMapping(value = "/{questionId}", method = RequestMethod.DELETE)
public void deleteInterposedQuestion(@ApiParam(value = "ID of the question that needs to be deleted", required = true) @PathVariable final String questionId) {
questionService.deleteInterposedQuestion(questionId);
public void deleteInterposedQuestion(@ApiParam(value = "ID of the comment that needs to be deleted", required = true) @PathVariable final String questionId) {
contentService.deleteInterposedQuestion(questionId);
}
}
......@@ -17,7 +17,7 @@
*/
package de.thm.arsnova.controller;
import de.thm.arsnova.services.IQuestionService;
import de.thm.arsnova.services.IContentService;
import de.thm.arsnova.web.DeprecatedApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
......@@ -33,7 +33,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
public class LegacyController extends AbstractController {
@Autowired
private IQuestionService questionService;
private IContentService contentService;
/* specific routes */
......@@ -95,7 +95,7 @@ public class LegacyController extends AbstractController {
@RequestMapping(value = "/session/{sessionKey}/interposed", method = RequestMethod.DELETE)
@ResponseBody
public void deleteAllInterposedQuestions(@PathVariable final String sessionKey) {
questionService.deleteAllInterposedQuestions(sessionKey);
contentService.deleteAllInterposedQuestions(sessionKey);
}
@DeprecatedApi
......
......@@ -298,7 +298,7 @@ public class SessionController extends PaginationController {
public List<ImportExportSession> getExport(
@ApiParam(value = "sessionkey", required = true) @RequestParam(value = "sessionkey", defaultValue = "") final List<String> sessionkey,
@ApiParam(value = "wether statistics shall be exported", required = true) @RequestParam(value = "withAnswerStatistics", defaultValue = "false") final Boolean withAnswerStatistics,
@ApiParam(value = "wether interposed questions shall be exported", required = true) @RequestParam(value = "withFeedbackQuestions", defaultValue = "false") final Boolean withFeedbackQuestions,
@ApiParam(value = "wether comments shall be exported", required = true) @RequestParam(value = "withFeedbackQuestions", defaultValue = "false") final Boolean withFeedbackQuestions,
final HttpServletResponse response
) {
List<ImportExportSession> sessions = new ArrayList<>();
......
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
* This file is part of ARSnova Backend.
* Copyright (C) 2012-2017 The ARSnova Team
*
* ARSnova Backend 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 Backend 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 com.fourspaces.couchdb.View;
import de.thm.arsnova.connector.model.Course;
import de.thm.arsnova.domain.CourseScore;
import de.thm.arsnova.entities.*;
import de.thm.arsnova.entities.transport.ImportExportSession;
import java.util.List;
import java.util.Map;
/**
* All methods the database must support.
*/
public interface IDatabaseDao {
/**
* Logs an event to the database. Arbitrary data can be attached as payload. Database logging should only be used
* if the logged data is later analyzed by the backend itself. Otherwise use the default logging mechanisms.
*
* @param event type of the event
* @param payload arbitrary logging data
* @param level severity of the event
*/
void log(String event, Map<String, Object> payload, LogEntry.LogLevel level);
/**
* Logs an event of informational severity to the database. Arbitrary data can be attached as payload. Database
* logging should only be used if the logged data is later analyzed by the backend itself. Otherwise use the default
* logging mechanisms.
*
* @param event type of the event
* @param payload arbitrary logging data
*/
void log(String event, Map<String, Object> payload);
/**
* Logs an event to the database. Arbitrary data can be attached as payload. Database logging should only be used
* if the logged data is later analyzed by the backend itself. Otherwise use the default logging mechanisms.
*
* @param event type of the event
* @param level severity of the event
* @param rawPayload key/value pairs of arbitrary logging data
*/
void log(String event, LogEntry.LogLevel level, Object... rawPayload);
/**
* Logs an event of informational severity to the database. Arbitrary data can be attached as payload. Database
* logging should only be used if the logged data is later analyzed by the backend itself. Otherwise use the default
* logging mechanisms.
*
* @param event type of the event
* @param rawPayload key/value pairs of arbitrary logging data
*/
void log(String event, Object... rawPayload);
Session getSessionFromKeyword(String keyword);
List<Session> getMySessions(User user, final int start, final int limit);
List<Session> getSessionsForUsername(String username, final int start, final int limit);
List<Session> getPublicPoolSessions();
List<Session> getMyPublicPoolSessions(User user);
Session saveSession(User user, Session session);
boolean sessionKeyAvailable(String keyword);
Question saveQuestion(Session session, Question question);
InterposedQuestion saveQuestion(Session session, InterposedQuestion question, User user);
Question getQuestion(String id);
List<Question> getSkillQuestionsForUsers(Session session);
List<Question> getSkillQuestionsForTeachers(Session session);
int getSkillQuestionCount(Session session);
LoggedIn registerAsOnlineUser(User u, Session s);
Session updateSessionOwnerActivity(Session session);
List<String> getQuestionIds(Session session, User user);
int deleteQuestionWithAnswers(Question question);
int[] deleteAllQuestionsWithAnswers(Session session);
List<String> getUnAnsweredQuestionIds(Session session, User user);
Answer getMyAnswer(User me, String questionId, int piRound);
List<Answer> getAnswers(Question question, int piRound);
List<Answer> getAnswers(Question question);
List<Answer> getAllAnswers(Question question);
int getAnswerCount(Question question, int piRound);
int getTotalAnswerCountByQuestion(Question question);
int getAbstentionAnswerCount(String questionId);
List<Answer> getFreetextAnswers(String questionId, final int start, final int limit);
List<Answer> getMyAnswers(User me, Session session);
int getTotalAnswerCount(String sessionKey);
int getInterposedCount(String sessionKey);
InterposedReadingCount getInterposedReadingCount(Session session);
InterposedReadingCount getInterposedReadingCount(Session session, User user);
List<InterposedQuestion> getInterposedQuestions(Session session, final int start, final int limit);
List<InterposedQuestion> getInterposedQuestions(Session session, User user, final int start, final int limit);
InterposedQuestion getInterposedQuestion(String questionId);
void markInterposedQuestionAsRead(InterposedQuestion question);
List<Session> getMyVisitedSessions(User user, final int start, final int limit);
List<Session> getVisitedSessionsForUsername(String username, final int start, final int limit);
Question updateQuestion(Question question);
int deleteAnswers(Question question);
Answer saveAnswer(Answer answer, User user, Question question, Session session);
Answer updateAnswer(Answer answer);
Session getSessionFromId(String sessionId);
void deleteAnswer(String answerId);
void deleteInterposedQuestion(InterposedQuestion question);
List<Session> getCourseSessions(List<Course> courses);
Session updateSession(Session session);
Session changeSessionCreator(Session session, String newCreator);
/**
* Deletes a session and related data.
*
* @param session the session for deletion
*/
int[] deleteSession(Session session);
int[] deleteInactiveGuestSessions(long lastActivityBefore);
int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore);
List<Question> getLectureQuestionsForUsers(Session session);
List<Question> getLectureQuestionsForTeachers(Session session);
List<Question> getFlashcardsForUsers(Session session);
List<Question> getFlashcardsForTeachers(Session session);
List<Question> getPreparationQuestionsForUsers(Session session);
List<Question> getPreparationQuestionsForTeachers(Session session);
List<Question> getAllSkillQuestions(Session session);
int getLectureQuestionCount(Session session);
int getFlashcardCount(Session session);
int getPreparationQuestionCount(Session session);
int countLectureQuestionAnswers(Session session);
int countPreparationQuestionAnswers(Session session);
int[] deleteAllLectureQuestionsWithAnswers(Session session);
int[] deleteAllFlashcardsWithAnswers(Session session);
int[] deleteAllPreparationQuestionsWithAnswers(Session session);
List<String> getUnAnsweredLectureQuestionIds(Session session, User user);
List<String> getUnAnsweredPreparationQuestionIds(Session session, User user);
int deleteAllInterposedQuestions(Session session);
int deleteAllInterposedQuestions(Session session, User user);
void publishQuestions(Session session, boolean publish, List<Question> questions);
List<Question> publishAllQuestions(Session session, boolean publish);
int deleteAllQuestionsAnswers(Session session);
DbUser createOrUpdateUser(DbUser user);
DbUser getUser(String username);
boolean deleteUser(DbUser dbUser);
int deleteInactiveUsers(long lastActivityBefore);
CourseScore getLearningProgress(Session session);
List<SessionInfo> getMySessionsInfo(User user, final int start, final int limit);
List<SessionInfo> getPublicPoolSessionsInfo();
List<SessionInfo> getMyPublicPoolSessionsInfo(final User user);
List<SessionInfo> getMyVisitedSessionsInfo(User currentUser, final int start, final int limit);
int deleteAllPreparationAnswers(Session session);
int deleteAllLectureAnswers(Session session);
SessionInfo importSession(User user, ImportExportSession importSession);
ImportExportSession exportSession(String sessionkey, Boolean withAnswer, Boolean withFeedbackQuestions);
Statistics getStatistics();
List<String> getSubjects(Session session, String questionVariant);
List<String> getQuestionIdsBySubject(Session session, String questionVariant, String subject);
List<Question> getQuestionsByIds(List<String> ids, Session session);
void resetQuestionsRoundState(Session session, List<Question